Sobrecarga de múltiples objetos de función por referencia


En C++17, es trivial implementar una función overload(fs...) que, dado cualquier número de argumentos fs... satisfaga 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 Vivo en wandbox


Dado que el anterior overloader hereda de Ts..., necesita copiar o mover los objetos de la función para funcionar. Quiero algo que proporcione la misma sobrecarga comportamiento, 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 de la siguiente manera:

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

Parece bastante 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 Vivo en wandbox

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

En para usar la sintaxis using Ts::operator()..., necesito Ts... para satisfacer FunctionObject. Si trato de hacer mi propio FunctionObject wrapper, me encuentro con el mismo problema:

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

Dado que no hay forma de expresar " compilador, por favor rellene el ??? con los mismos argumentos que TF::operator()", Necesito usar una plantilla de función variádica , sin resolver nada.

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

Por lo tanto, mi pregunta es: ¿hay una forma de implementar una función ref_overload(fs...) que, dado cualquier número de objetos de función fs..., devuelve un nuevo objeto de función que se comporta como una sobrecarga de fs..., pero se refiere a fs... en lugar de copiarlos/moverlos?

Author: Vittorio Romeo, 2017-03-24

2 answers

Muy bien, este es el plan: vamos a determinar qué objeto de función contiene la sobrecarga operator() que se elegiría si usáramos un sobrecargador básico basado en la herencia y el uso de declaraciones, como se ilustra en la pregunta. Vamos a hacer eso (en un contexto no evaluado) forzando una ambigüedad en la conversión derivada a base para el parámetro objeto implícito, que ocurre después de que la resolución de sobrecarga tenga éxito. Este comportamiento está especificado en la norma, véase N4659 [espacio de nombres.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 tiene éxito, crear una ambigüedad de base para cualquiera de los objetos de función que no contienen la sobrecarga ganadora no cambiará nada (la llamada seguirá teniendo é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 con. A continuación, 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:

  • Asumimos que, aunque no queremos un sobrecargador basado en la herencia, podríamos heredar de esos objetos de función si quisiéramos. No se crea ningún objeto derivado, pero las comprobaciones realizadas en contextos no evaluados se basan en que esto sea posible. No puedo pensar en otra manera de traer esas sobrecargas en el mismo alcance para que la resolución de sobrecarga se puede aplicar a ellos.
  • Asumimos 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 sin algún tipo de reenvío, por lo que parece un requisito obligatorio.
  • Esto funciona actualmente en Clang. Para GCC, parece que la conversión derivada a base en la que estamos confiando no es un contexto SFINAE, por lo que desencadena un error duro; esto es incorrecto por lo que puedo decir. MSVC es muy útil y desambiga la llamada para nosotros: parece que solo elige el subobjeto de la clase base que sucede que viene primero; allí, funciona - ¿qué no le gusta? (MSVC es menos relevante para nuestro problema en este momento, ya que tampoco admite otras características de C++17).
  • La composición funciona a través de algunas precauciones especiales: al probar el sobrecargador hipotético basado en la herencia, un ref_overloader se desenvuelve en su función constituyente objetos, para que sus operator()s participen en la resolución de sobrecarga en lugar del reenvío operator(). Cualquier otro sobrecargador que intente componer ref_overloaders obviamente fallará a menos que haga algo similar.

Algunos bits útiles:

  • Un buen ejemplo simplificado por Vittorio mostrando la idea base ambigua en acción.
  • Sobre la implementación de add_base: la especialización parcial de over_base para ref_overloader hace el" desenvolvimiento " mencionado arriba para habilitar ref_overloader s que contienen otros ref_overloader s. Con eso en su lugar, solo lo reutilicé para construir add_base, que es un poco de un truco, lo admito. add_base está realmente destinado a ser algo 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 fallaría para el caso normal (sin base ambigua añadida), entonces también fallaría para todos los casos "instrumentados", independientemente de lo que base se añade, por lo que la matriz checks contendría solo elementos true. Por el contrario, si la resolución de sobrecarga tendría éxito para el caso normal, entonces también tendría éxito para todos los demás casos excepto uno, por lo que checks contendría un elemento true con todos los demás iguales a false.

    Dada esta uniformidad en los valores en checks, podemos mirar solo los dos primeros elementos: si ambos son true, esto indica un fallo en la resolución de sobrecarga en el caso normal; todas las demás combinaciones indique el éxito de la resolución. Esta es la solución perezosa; en una implementación de producción, probablemente haría una prueba exhaustiva para verificar que checks realmente contiene una configuración esperada.


Informe de error para GCC , enviado por Vittorio.

Informe de error para MSVC .

 28
Author: bogdan,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-04-08 17:21:32

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

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ías hacer que eso funcione? Podríamos comprobar que ambos tipos son llamables con int*, pero ambos operator() son plantillas por lo que no podemos seleccionar sus firmas. Incluso si pudiéramos, los parámetros deducidos son idénticos-ambas funciones toman un int*. ¿Cómo sabrías llamar b?

Con el fin de obtener este caso correcto, lo que básicamente lo que hay que hacer es inyectar el tipo de retorno en los operadores de llamada. 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.

En el caso más simple - cuando ambos A y B (y C y ...) tener una única operator() que no sea una plantilla, esto es factible, ya que en realidad podemos inspeccionar &X::operator() y manipular esas firmas para producir las nuevas que necesitamos. Esto nos permite seguir utilizando el compilador para hacer resolución de sobrecarga para nosotros.

También podemos comprobar qué tipo overload(declval<A>(), declval<B>(), ...)(args...) rendimientos. Si el tipo de retorno de la mejor coincidencia es único en casi todos los candidatos viables, todavía 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 se han sobrecargado o operadores de llamada templados con el mismo tipo de retorno, necesitamos algo más. Necesitamos algunas características futuras del lenguaje.

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

Una posible solución alternativa en el futuro sería utilizar overloaded operator .. Sección 4.12 incluye un ejemplo que indica que el diseño permite sobrecarga de diferentes funciones miembro por nombre a través de diferentes operator.() s. Si esa propuesta pasa en alguna forma similar hoy, entonces la implementación de la sobrecarga de referencia seguiría el mismo patrón que la sobrecarga de objeto hoy, simplemente sustituyendo diferentes operator .() s por las diferentes operator ()s de hoy:

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?
};
 10
Author: Barry,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-03-27 15:36:03