c++ - unarios - Sobrecarga de objetos de función múltiple por referencia




sobrecarga de operadores unarios en c++ (2)

En C ++ 17, es trivial implementar una función de overload(fs...) que, dada cualquier cantidad de argumentos fs... satisfagan FunctionObject , devuelve un nuevo objeto de función que se comporta como una sobrecarga de fs... Ejemplo:

template <typename... Ts>
struct overloader : Ts...
{
    template <typename... TArgs>
    overloader(TArgs&&... xs) : Ts{forward<TArgs>(xs)}...
    {
    }

    using Ts::operator()...;
};

template <typename... Ts>
auto overload(Ts&&... xs)
{
    return overloader<decay_t<Ts>...>{forward<Ts>(xs)...};
}

int main()
{
    auto o = overload([](char){ cout << "CHAR"; }, 
                      [](int) { cout << "INT";  });

    o('a'); // prints "CHAR"
    o(0);   // prints "INT"
}

ejemplo en vivo en wandbox

Como el overloader anterior hereda de Ts... , necesita copiar o mover los objetos de función para funcionar. Quiero algo que proporcione el mismo comportamiento de sobrecarga, pero solo referencias a los objetos de función pasados.

Llamemos a esa función hipotética ref_overload(fs...) . Mi intento fue usar std::reference_wrapper y std::ref siguiente manera:

template <typename... Ts>
auto ref_overload(Ts&... xs)
{
    return overloader<reference_wrapper<Ts>...>{ref(xs)...};
}

Parece lo suficientemente simple, ¿verdad?

int main()
{
    auto l0 = [](char){ cout << "CHAR"; };
    auto l1 = [](int) { cout << "INT";  };

    auto o = ref_overload(l0, l1);

    o('a'); // BOOM
    o(0);
}

error: call of '(overloader<...>) (char)' is ambiguous
 o('a'); // BOOM
      ^

ejemplo en vivo en wandbox

La razón por la que no funciona es simple: std::reference_wrapper::operator() es una plantilla de función variadica , que no funciona bien con la sobrecarga .

Para usar la sintaxis using Ts::operator()... , necesito Ts... para satisfacer FunctionObject . Si intento crear mi propia envoltura FunctionObject , encuentro el mismo problema:

template <typename TF>
struct function_ref
{
    TF& _f;
    decltype(auto) operator()(/* ??? */);
};

Como no hay forma de expresar "compilador, complete el ??? con exactamente los mismos argumentos que TF::operator() " , necesito usar una plantilla de función variadica , sin resolver nada.

Tampoco puedo usar algo como boost::function_traits porque una de las funciones pasadas a la overload(...) puede ser una plantilla de función o un objeto de función sobrecargado .

Por lo tanto, mi pregunta es: ¿hay alguna manera de implementar una función ref_overload(fs...) que, dada cualquier cantidad de objetos de función fs... , devuelva un nuevo objeto de función que se comporte como una sobrecarga de fs... , pero se refiere a fs... lugar de copiarlos / moverlos?


En el caso general, no creo que tal cosa sea posible incluso en C ++ 17. Considera el caso más desagradable:

struct A {
    template <class T> int operator()(T );
} a;

struct B {
    template <class T> int operator()(T* );
} b;

ref_overload(a, b)(new int);

¿Cómo podría hacer que eso funcione? Podríamos verificar que ambos tipos puedan invocarse con int* , pero ambos operator() s son plantillas, por lo que no podemos seleccionar sus firmas. Incluso si pudiéramos, los parámetros deducidos son idénticos: ambas funciones toman una int* . ¿Cómo sabrías llamar a b ?

Para que este caso sea correcto, lo que básicamente tendrías que hacer es inyectar el tipo de devolución en los operadores de llamadas. Si pudiéramos crear tipos:

struct A' {
    template <class T> index_<0> operator()(T );
};

struct B' {
    template <class T> index_<1> operator()(T* );
};

Entonces podríamos usar decltype(overload(declval<A'>(), declval<B'>()))::value para elegir qué referencia llamarnos a nosotros mismos.

En el caso más simple : cuando A y B (y C y ...) tienen un solo operator() que no es una plantilla, esto es factible, ya que podemos inspeccionar &X::operator() y manipular esas firmas para producir los nuevos que necesitamos Esto nos permite seguir usando el compilador para hacer una resolución de sobrecarga para nosotros.

También podemos verificar qué tipo de overload(declval<A>(), declval<B>(), ...)(args...) cede. Si el tipo de devolución del mejor partido es exclusivo de casi todos los candidatos viables, aún podemos elegir la sobrecarga correcta en ref_overload . Esto cubrirá más terreno para nosotros, ya que ahora podemos manejar correctamente algunos casos con operadores de llamadas sobrecargados o con plantillas, pero rechazaremos incorrectamente muchas llamadas como ambiguas que no lo son.

Pero para resolver el problema general, con tipos que han sobrecargado o templado llaman a operadores con el mismo tipo de devolución, necesitamos algo más. Necesitamos algunas características del lenguaje futuro.

La reflexión completa nos permitiría inyectar un tipo de devolución como se describió anteriormente. No sé cómo se vería específicamente, pero espero ver la implementación de Yakk.

Una solución potencial futura alternativa sería usar un operator . sobrecargado operator . . La Sección 4.12 incluye un ejemplo que indica que el diseño permite la sobrecarga de diferentes funciones de miembro por nombre a través de diferentes operator.() S. Si esa propuesta pasa de forma similar hoy en día, entonces la implementación de la sobrecarga de referencia seguiría el mismo patrón que la sobrecarga de objetos hoy en día, simplemente sustituyendo diferentes operator .() Para los diferentes operator () :

template <class T>
struct ref_overload_one {
    T& operator.() { return r; }
    T& r;
};

template <class... Ts>
struct ref_overloader : ref_overload_one<Ts>...
{
    ref_overloader(Ts&... ts)
    : ref_overload_one<Ts>{ts}...
    { }

    using ref_overload_one<Ts>::operator....; // intriguing syntax?
};

Muy bien, aquí está el plan: vamos a determinar qué objeto de función contiene la sobrecarga del operator() que se elegiría si utilizamos un sobrecarga basado en la herencia y el uso de declaraciones, como se ilustra en la pregunta. Vamos a hacer eso (en un contexto no evaluado) al forzar una ambigüedad en la conversión derivada a base para el parámetro de objeto implícito, que ocurre después de que la resolución de sobrecarga tiene éxito. Este comportamiento se especifica en el estándar, vea N4659 [namespace.udecl]/16 y 18 .

Básicamente, vamos a agregar cada objeto de función a su vez como un subobjeto de clase base adicional. Para una llamada para la que la resolución de sobrecarga sea exitosa, la creación de una ambigüedad básica para cualquiera de los objetos de función que no contengan la sobrecarga ganadora no cambiará nada (la llamada aún tendrá éxito). Sin embargo, la llamada fallará en el caso en que la base duplicada contenga la sobrecarga elegida. Esto nos da un contexto SFINAE para trabajar. Luego reenviamos la llamada a través de la referencia correspondiente.

#include <cstddef>
#include <type_traits>
#include <tuple>
#include <iostream>

template<class... Ts> 
struct ref_overloader
{
   static_assert(sizeof...(Ts) > 1, "what are you overloading?");

   ref_overloader(Ts&... ts) : refs{ts...} { }
   std::tuple<Ts&...> refs;

   template<class... Us> 
   decltype(auto) operator()(Us&&... us)
   {
      constexpr bool checks[] = {over_fails<Ts, pack<Us...>>::value...};
      static_assert(over_succeeds(checks), "overload resolution failure");
      return std::get<choose_obj(checks)>(refs)(std::forward<Us>(us)...);
   }

private:
   template<class...> 
   struct pack { };

   template<int Tag, class U> 
   struct over_base : U { };

   template<int Tag, class... Us> 
   struct over_base<Tag, ref_overloader<Us...>> : Us... 
   { 
       using Us::operator()...; // allow composition
   }; 

   template<class U> 
   using add_base = over_base<1, 
       ref_overloader<
           over_base<2, U>, 
           over_base<1, Ts>...
       >
   >&; // final & makes declval an lvalue

   template<class U, class P, class V = void> 
   struct over_fails : std::true_type { };

   template<class U, class... Us> 
   struct over_fails<U, pack<Us...>,
      std::void_t<decltype(
          std::declval<add_base<U>>()(std::declval<Us>()...)
      )>> : std::false_type 
   { 
   };

   // For a call for which overload resolution would normally succeed, 
   // only one check must indicate failure.
   static constexpr bool over_succeeds(const bool (& checks)[sizeof...(Ts)]) 
   { 
       return !(checks[0] && checks[1]); 
   }

   static constexpr std::size_t choose_obj(const bool (& checks)[sizeof...(Ts)])
   {
      for(std::size_t i = 0; i < sizeof...(Ts); ++i)
         if(checks[i]) return i;
      throw "something's wrong with overload resolution here";
   }
};

template<class... Ts> auto ref_overload(Ts&... ts)
{
   return ref_overloader<Ts...>{ts...};
}


// quick test; Barry's example is a very good one

struct A { template <class T> void operator()(T) { std::cout << "A\n"; } };
struct B { template <class T> void operator()(T*) { std::cout << "B\n"; } };

int main()
{
   A a;
   B b;
   auto c = [](int*) { std::cout << "C\n"; };
   auto d = [](int*) mutable { std::cout << "D\n"; };
   auto e = [](char*) mutable { std::cout << "E\n"; };
   int* p = nullptr;
   auto ro1 = ref_overload(a, b);
   ro1(p); // B
   ref_overload(a, b, c)(p); // B, because the lambda's operator() is const
   ref_overload(a, b, d)(p); // D
   // composition
   ref_overload(ro1, d)(p); // D
   ref_overload(ro1, e)(p); // B
}

ejemplo en vivo en wandbox

Advertencias:

  • Estamos asumiendo que, aunque no queremos un overloader basado en la herencia, podríamos heredar de esos objetos de función si quisiéramos. No se crea dicho objeto derivado, pero las comprobaciones realizadas en contextos no evaluados se basan en que esto sea posible. No puedo pensar en ninguna otra forma de poner esas sobrecargas en el mismo ámbito para que se les pueda aplicar la resolución de sobrecarga.
  • Estamos asumiendo que el reenvío funciona correctamente para los argumentos de la llamada. Dado que tenemos referencias a los objetos de destino, no veo cómo podría funcionar esto sin algún tipo de reenvío, por lo que parece un requisito obligatorio.
  • Esto actualmente funciona en Clang. Para GCC, parece que la conversión derivada a base en la que confiamos no es un contexto SFINAE, por lo que desencadena un error grave; esto es incorrecto por lo que puedo decir. MSVC es muy útil y desambigua la llamada para nosotros: parece que simplemente elige el subobjeto de clase base que pasa primero; allí, funciona, ¿qué no le gusta? (MSVC es menos relevante para nuestro problema en este momento, ya que no es compatible con otras características de C ++ 17 tampoco).
  • La composición funciona a través de algunas precauciones especiales: cuando se prueba el ref_overloader hipotético basado en herencia, un ref_overloader se ref_overloader en sus objetos de función constituyentes, de modo que su operator() participe en la resolución de sobrecarga en lugar del operator() reenvío operator() . Cualquier otro overloader que intente componer ref_overloader s obviamente fallará a menos que haga algo similar.

Algunos bits útiles:

  • Un buen ejemplo simplificado de muestra la idea base ambigua en acción.
  • Acerca de la implementación de add_base : la especialización parcial de over_base para ref_overloader hace el "unwrapping" mencionado anteriormente para habilitar ref_overloader s que contiene otros ref_overloader s. Con eso en su lugar, lo reutilicé para construir add_base , lo cual es un poco add_base , lo admito. add_base está destinado a ser algo así como inheritance_overloader<over_base<2, U>, over_base<1, Ts>...> , pero no quería definir otra plantilla que hiciera lo mismo.
  • Acerca de esa extraña prueba en over_succeeds : la lógica es que si la resolución de sobrecarga fallara para el caso normal (sin una base ambigua añadida), también fallaría para todos los casos "instrumentados", independientemente de la base que se agregue, de modo que los checks array contendría solo elementos true . Por el contrario, si la resolución de sobrecarga tuviera éxito para el caso normal, también tendría éxito para todos los demás casos, excepto uno, por lo que los checks contendrían un elemento true con todos los demás igual a false .

    Dada esta uniformidad en los valores en las checks , podemos ver solo los dos primeros elementos: si ambos son true , esto indica una falla de resolución de sobrecarga en el caso normal; todas las otras combinaciones indican el éxito de la resolución. Esta es la solución perezosa; en una implementación de producción, probablemente iría a una prueba integral para verificar que las checks realmente contengan una configuración esperada.

Informe de errores para GCC , presentado por .

Informe de error para MSVC .





function-object