39 votes

La surcharge de la fonction multiple d'objets par référence

En C++17, c'est facile à mettre en œuvre un overload(fs...) fonction qui, étant donné un nombre quelconque d'arguments fs... satisfaire FunctionObject, renvoie une nouvelle fonction de l'objet qui se comporte comme une surcharge de fs.... Exemple:

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"
}

live exemple sur wandbox


Depuis la overloader hérite Ts..., il a besoin de copier ou de déplacer la fonction des objets dans le but de travailler. Je veux quelque chose qui fournit la même surcharge de comportement, mais seulement des références au passé de la fonction des objets.

Nous allons appeler cette fonction hypothétique ref_overload(fs...). Ma tentative a été l'aide d' std::reference_wrapper et std::ref comme suit:

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

Semble assez simple, non?

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
      ^

live exemple sur wandbox

La raison pour laquelle il n'a pas de travail est simple: std::reference_wrapper::operator() est un variadic modèle de fonction, qui ne joue pas bien avec la surcharge.

Afin d'utiliser l' using Ts::operator()... de la syntaxe, j'ai besoin d' Ts... pour satisfaire FunctionObject. Si j'essaie de faire mon propre FunctionObject wrapper, je rencontre le même problème:

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

Car il n'y a aucun moyen d'exprimer "compilateur, merci de remplir le ??? avec exactement les mêmes arguments que l' TF::operator()", j'ai besoin d'utiliser un variadic fonction du modèle, de la résolution de rien.

Je ne peux pas utiliser quelque chose comme boost::function_traits parce que l'une des fonctions passé de overload(...) peut être un modèle de fonction ou d'une fonction surchargée de l'objet lui-même!

Donc, ma question est: est-il un moyen de mettre en œuvre un ref_overload(fs...) fonction qui, étant donné un nombre quelconque d' fs... objets de fonction, retourne une nouvelle fonction de l'objet qui se comporte comme une surcharge de fs..., mais se réfère fs... au lieu de copier/déplacer?

28voto

bogdan Points 7816

Tout à droite, voici le plan: nous allons déterminer la fonction de l'objet contient l' operator() surcharge qui serait choisi, si nous avons utilisé un bare-bones overloader basé sur l'héritage et à l'aide de déclarations, comme illustré dans la question. Nous allons le faire (non évalué contexte) en forçant une ambiguïté dans les dérivés à base de conversion de l'objet implicite de paramètre, ce qui se passe après la résolution de surcharge réussit. Ce comportement est spécifié dans la norme, voir N4659 [espace de noms.udecl]/16 et 18.

Fondamentalement, nous allons ajouter la fonction de chaque objet à son tour comme un autre de la classe de base sous-objet. Pour un appel de résolution de surcharge réussit, la création d'une base de l'ambiguïté pour toute fonction d'objets qui ne contiennent pas de la réussite de surcharge ne va pas changer quoi que ce soit (l'appel de réussite). Toutefois, l'appel échouera pour le cas où la copie de la base contient les choisi de surcharge. Cela nous donne une SFINAE contexte de travailler avec. Nous avons ensuite transférer l'appel par le biais de la référence correspondante.

#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
}

live exemple sur wandbox


Mises en garde:

  • Nous supposons que, même si nous ne voulons pas d'un overloader basé sur l'héritage, nous avons pu hériter de ces objets de fonction si nous le voulions. Aucun de ces dérivés de l'objet est créée, mais les vérifications faites dans des contextes non évaluée compter de ce qui est possible. Je ne peux pas penser à une autre façon de faire ces surcharges dans le même champ d'application, de sorte que la résolution de surcharge peut être appliquée.
  • Nous sommes en supposant que le transfert fonctionne correctement pour les arguments de l'appel. Étant donné que nous avons des références aux objets cible, je ne vois pas comment cela pourrait fonctionner sans une sorte de transfert, de sorte que cela semble comme une exigence obligatoire.
  • Actuellement, il travaille sur Clang. Pour GCC, il semble que la dérivée à la base de la conversion, nous sommes en s'appuyant sur n'est pas une SFINAE contexte, il déclenche une erreur matérielle; c'est incorrect car autant que je peux dire. MSVC est super utile et disambiguates l'appel pour nous: on dirait juste choisit la classe de base sous-objet qui vient en premier; il, il fonctionne, quoi de ne pas aimer? (MSVC est de moins en moins appropriée à notre problème en ce moment, car il ne supporte pas les autres C++17 fonctionnalités).
  • La Composition fonctionne grâce à des précautions particulières - lors de l'essai de l'hypothétique héritage basé overloader, un ref_overloader est déballé dans sa fonction constitutive des objets, de sorte que leur operator()s participer à la résolution de surcharge à la place de la redirection operator(). Toute autre overloader de tenter de composer ref_overloaders va évidemment pas, sauf si il fait quelque chose de similaire.

Certains bits utiles:

  • Un bel exemple simplifié par Vittorio montrant l'ambiguïté de la base de l'idée à l'action.
  • À propos de la mise en œuvre de l' add_base: la spécialisation partielle de l' over_base pour ref_overloader le "déballage" mentionné ci-dessus pour activer ref_overloaders contenant d'autres ref_overloaders. Avec cela en place, je viens de réutilisés pour construire add_base, ce qui est un peu un hack, je l'admets. add_base est vraiment censé être quelque chose comme inheritance_overloader<over_base<2, U>, over_base<1, Ts>...>, mais je ne voulais pas de définir un autre modèle qui ferait la même chose.
  • À propos de cette étrange test en over_succeeds: la logique est que si la résolution de surcharge serait un échec pour le cas normal (pas d'ambigu de base ajoutée), alors il serait également échouer pour toutes les "instrumenté" les cas, peu importe ce que la base est ajouté, de sorte que l' checks tableau ne contient qu' true - éléments. A l'inverse, si la résolution de surcharge serait un succès pour le cas normal, alors il faudrait aussi réussir pour tous les autres cas, sauf un, alors checks contiendrait une true élément avec tous les autres d'égal à false.

    Compte tenu de cette uniformité dans les valeurs en checks, nous pouvons regarder juste les deux premiers éléments: si les deux sont true, ce qui indique une surcharge échec de la résolution dans le cas normal; toutes les autres combinaisons indiquer la résolution de succès. C'est le paresseux solution; dans une production mise en œuvre, je serais probablement aller pour un test complet, pour vérifier qu' checks contient réellement une configuration attendue.


Rapport de Bug pour GCC, présenté par Vittorio.

Rapport de Bug pour MSVC.

10voto

Barry Points 45207

Dans le cas général, je ne pense pas qu'une telle chose est possible, même en C++17. Considérez comme le plus odieux des cas:

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

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

ref_overload(a, b)(new int);

Comment pourriez-vous éventuellement effectuer ce travail? Nous avons pu vérifier que les deux types sont disponibles avec int*, mais les deux operator()s sont des modèles on ne peut donc pas choisir leurs signatures. Même si on pourrait le déduire les paramètres sont identiques - les deux fonctions prennent un int*. Comment voulez-vous savoir à l'appel b?

Afin d'obtenir ce cas, corriger, ce que vous ne devez faire est d'injecter le type de retour à l'appel des opérateurs. Si nous pouvions créer des types:

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

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

Puis nous avons pu utiliser decltype(overload(declval<A'>(), declval<B'>()))::value de choisir la référence de nous appeler.

Dans le plus simple des cas, lorsque les deux A et B (et C et ...) de disposer d'un seul operator() qui n'est pas un modèle, c'est faisable - puisque nous ne pouvons inspecter &X::operator() et de manipuler ces signatures pour produire les nouveaux que nous avons besoin. Cela nous permet de toujours utiliser le compilateur de faire de la résolution de surcharge pour nous.

Nous pouvons également vérifier quel type overload(declval<A>(), declval<B>(), ...)(args...) des rendements. Si le meilleur match de retour de ce type est unique presque tous les candidats viables, nous pouvons encore choisir le bon surcharge en ref_overload. Cela permettra de couvrir plus de terrain pour nous, que nous pouvons maintenant gérer correctement certains cas de surcharge ou d'basé sur un modèle d'appel des opérateurs, mais nous avons tort de rejeter des appels ambigus qui ne le sont pas.


Mais afin de résoudre le problème général, avec des types qui ont surchargé ou basé sur un modèle opérateurs d'appel avec le même type de retour, nous avons besoin de quelque chose de plus. Nous avons besoin de quelques futures fonctionnalités de la langue.

Pleine réflexion nous permettra d'injecter un type de retour, comme décrit ci-dessus. Je ne sais pas de quoi ça pourrait ressembler, mais j'ai hâte de voir Yakk de mise en œuvre.

Une alternative potentielle future solution serait d'utiliser surchargé operator .. La Section 4.12 comprend un exemple qui indique que la conception permet de surcharger les différentes fonctions de membre par son nom par le biais de différents operator.()s. Si cette proposition passe dans une forme analogue aujourd'hui, la mise en œuvre de référence-la surcharge serait-il le même que l'objet de surcharge aujourd'hui, juste substituer différents operator .()s pour aujourd'hui est différente operator ()s:

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?
};

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X