Pourquoi std :: function ne participe-t-il pas à la résolution de surcharge?

17

Je sais que le code suivant ne se compilera pas.

void baz(int i) { }
void baz() {  }


class Bar
{
    std::function<void()> bazFn;
public:
    Bar(std::function<void()> fun = baz) : bazFn(fun){}

};

int main(int argc, char **argv)
{
    Bar b;
    return 0;
}

Parce std::functionqu'on dit de ne pas considérer la résolution de surcharge, comme je l'ai lu dans cet autre post .

Je ne comprends pas bien les limites techniques qui ont forcé ce type de solution.

J'ai lu sur les phases de traduction et les modèles sur cppreference, mais je ne peux penser à aucun raisonnement auquel je n'ai pas trouvé de contre-exemple. Expliqué à un demi-profane (encore nouveau en C ++), quoi et pendant quelle étape de la traduction les éléments ci-dessus ne parviennent-ils pas à être compilés?

TuRtoise
la source
1
@Evg mais il n'y a qu'une seule surcharge qui aurait du sens pour être acceptée dans l'exemple OPs. Dans votre exemple, les deux feraient un match
idclev 463035818

Réponses:

13

Cela n'a rien à voir avec les "phases de traduction". Il s'agit uniquement des constructeurs de std::function.

Voir, std::function<R(Args)>ne nécessite pas que la fonction donnée soit exactement du type R(Args). En particulier, il ne nécessite pas de disposer d'un pointeur de fonction. Il peut prendre n'importe quel type appelable (pointeur de fonction membre, un objet qui a une surcharge de operator()) tant qu'il est invocable comme s'il prenait des Argsparamètres et renvoie quelque chose de convertible en R (ou s'il l' Rest void, il peut retourner n'importe quoi).

Pour ce faire, le constructeur approprié de std::functiondoit être un modèle : template<typename F> function(F f);. Autrement dit, il peut prendre n'importe quel type de fonction (sous réserve des restrictions ci-dessus).

L'expression bazreprésente un ensemble de surcharge. Si vous utilisez cette expression pour appeler l'ensemble de surcharge, c'est très bien. Si vous utilisez cette expression comme paramètre d'une fonction qui prend un pointeur de fonction spécifique, C ++ peut réduire la surcharge définie en un seul appel, ce qui le rend très bien.

Cependant, une fois qu'une fonction est un modèle et que vous utilisez la déduction d'arguments de modèle pour déterminer ce qu'est ce paramètre, C ++ n'a plus la possibilité de déterminer la surcharge correcte dans l'ensemble de surcharge. Vous devez donc le spécifier directement.

Nicol Bolas
la source
Pardonnez-moi si je me trompe, mais comment se fait-il que C ++ n'ait pas la capacité de distinguer les surcharges disponibles? Je vois maintenant que std :: function <T> accepte tout type compatible, non seulement une correspondance exacte, mais une seule de ces deux surcharges baz () est invocable comme si elle prenait des paramètres spécifiés. Pourquoi est-il impossible de lever l'ambiguïté?
TuRtoise
Parce que tout C ++ voit est la signature que j'ai citée. Il ne sait pas à quoi il est censé correspondre. Essentiellement, chaque fois que vous utilisez un ensemble de surcharge par rapport à tout ce qui n'est pas évident, quelle est la bonne réponse explicitement du code C ++ (le code étant cette déclaration de modèle), le langage vous oblige à expliquer ce que vous voulez dire.
Nicol Bolas
1
@TuRtoise: Le paramètre de modèle sur le functionmodèle de classe n'est pas pertinent . Ce qui importe, c'est le paramètre de modèle sur le constructeur que vous appelez. Ce qui est juste typename F: aka, n'importe quel type.
Nicol Bolas
1
@TuRtoise: Je pense que vous vous méprenez sur quelque chose. C'est "juste F" parce que c'est comme ça que les modèles fonctionnent. À partir de la signature, ce constructeur de fonction prend n'importe quel type, donc toute tentative de l'appeler avec la déduction d'argument de modèle déduira le type du paramètre. Toute tentative d'application d'une déduction à un jeu de surcharge est une erreur de compilation. Le compilateur n'essaie pas de déduire tous les types possibles de l'ensemble pour voir lesquels fonctionnent.
Nicol Bolas
1
"Toute tentative d'appliquer la déduction à un ensemble de surcharge est une erreur de compilation. Le compilateur n'essaie pas de déduire tous les types possibles de l'ensemble pour voir lesquels fonctionnent." Exactement ce qui me manquait, merci :)
TuRtoise
7

La résolution de surcharge se produit uniquement lorsque (a) vous appelez le nom d'une fonction / opérateur, ou (b) vous le convertissez en un pointeur (vers une fonction ou une fonction membre) avec une signature explicite.

Ni l'un ni l'autre ne se produit ici.

std::functionprend tout objet compatible avec sa signature. Il ne prend pas spécifiquement un pointeur de fonction. (une lambda n'est pas une fonction std, et une fonction std n'est pas une lambda)

Maintenant, dans mes variantes de fonction homebrew, pour la signature, R(Args...)j'accepte également un R(*)(Args...)argument (une correspondance exacte) pour exactement cette raison. Mais cela signifie qu'il élève les signatures de "correspondance exacte" au-dessus des signatures "compatibles".

Le problème principal est qu'un ensemble de surcharge n'est pas un objet C ++. Vous pouvez nommer un jeu de surcharge, mais vous ne pouvez pas le faire passer "nativement".

Maintenant, vous pouvez créer un ensemble de pseudo-surcharge d'une fonction comme celle-ci:

#define RETURNS(...) \
  noexcept(noexcept(__VA_ARGS__)) \
  -> decltype(__VA_ARGS__) \
  { return __VA_ARGS__; }

#define OVERLOADS_OF(...) \
  [](auto&&...args) \
  RETURNS( __VA_ARGS__(decltype(args)(args)...) )

cela crée un seul objet C ++ qui peut effectuer une résolution de surcharge sur un nom de fonction.

En développant les macros, nous obtenons:

[](auto&&...args)
noexcept(noexcept( baz(decltype(args)(args)...) ) )
-> decltype( baz(decltype(args)(args)...) )
{ return baz(decltype(args)(args)...); }

ce qui est ennuyeux à écrire. Une version plus simple, mais un peu moins utile, est ici:

[](auto&&...args)->decltype(auto)
{ return baz(decltype(args)(args)...); }

nous avons un lambda qui prend n'importe quel nombre d'arguments, puis parfait les transmet à baz.

Alors:

class Bar {
  std::function<void()> bazFn;
public:
  Bar(std::function<void()> fun = OVERLOADS_OF(baz)) : bazFn(fun){}
};

travaux. Nous reportons la résolution de surcharge dans le lambda dans lequel nous stockons fun, au lieu de passer fundirectement un ensemble de surcharge (qu'il ne peut pas résoudre).

Il y a eu au moins une proposition pour définir une opération dans le langage C ++ qui convertit un nom de fonction en un objet d'ensemble de surcharge. Jusqu'à ce qu'une telle proposition standard soit dans la norme, la OVERLOADS_OFmacro est utile.

Vous pourriez aller plus loin et prendre en charge le pointeur de fonction de conversion en compatibilité.

struct baz_overloads {
  template<class...Ts>
  auto operator()(Ts&&...ts)const
  RETURNS( baz(std::forward<Ts>(ts)...) );

  template<class R, class...Args>
  using fptr = R(*)(Args...);
  //TODO: SFINAE-friendly support
  template<class R, class...Ts>
  operator fptr<R,Ts...>() const {
    return [](Ts...ts)->R { return baz(std::forward<Ts>(ts)...); };
  }
};

mais cela commence à devenir obtus.

Exemple en direct .

#define OVERLOADS_T(...) \
  struct { \
    template<class...Ts> \
    auto operator()(Ts&&...ts)const \
    RETURNS( __VA_ARGS__(std::forward<Ts>(ts)...) ); \
\
    template<class R, class...Args> \
    using fptr = R(*)(Args...); \
\
    template<class R, class...Ts> \
    operator fptr<R,Ts...>() const { \
      return [](Ts...ts)->R { return __VA_ARGS__(std::forward<Ts>(ts)...); }; \
    } \
  }
Yakk - Adam Nevraumont
la source
5

Le problème ici est qu'il n'y a rien qui indique au compilateur comment effectuer la fonction de décroissance du pointeur. Si tu as

void baz(int i) { }
void baz() {  }

class Bar
{
    void (*bazFn)();
public:
    Bar(void(*fun)() = baz) : bazFn(fun){}

};

int main(int argc, char **argv)
{
    Bar b;
    return 0;
}

Ensuite, le code fonctionnerait puisque maintenant le compilateur sait à quelle fonction vous voulez car il y a un type concret auquel vous assignez.

Lorsque vous utilisez, std::functionvous appelez son constructeur d'objet fonction qui a la forme

template< class F >
function( F f );

et puisqu'il s'agit d'un modèle, il doit déduire le type de l'objet transmis. comme il bazs'agit d'une fonction surchargée, aucun type unique ne peut être déduit, la déduction de modèle échoue et vous obtenez une erreur. Vous devez utiliser

Bar(std::function<void()> fun = (void(*)())baz) : bazFn(fun){}

pour forcer un seul type et permettre la déduction.

NathanOliver
la source
"puisque baz est une fonction surchargée, il n'y a pas de type unique qui puisse être déduit" mais depuis C ++ 14 "ce constructeur ne participe pas à la résolution de surcharge sauf si f est Callable pour les types d'arguments Args ... et retourne le type R" Je suis pas sûr, mais j'étais près de m'attendre à ce que ce soit suffisant pour résoudre l'ambiguïté
idclev 463035818
3
@ formerlyknownas_463035818 Mais pour le déterminer, il doit d'abord déduire un type, et ce n'est pas possible car c'est un nom surchargé.
NathanOliver
1

Au moment où le compilateur décide quelle surcharge passer dans le std::functionconstructeur, tout ce qu'il sait, c'est que le std::functionconstructeur est conçu pour prendre n'importe quel type. Il n'a pas la possibilité d'essayer les deux surcharges et de constater que le premier ne compile pas mais le second le fait.

La façon de résoudre ce problème est d'indiquer explicitement au compilateur la surcharge que vous souhaitez avec un static_cast:

Bar(std::function<void()> fun = static_cast<void(*)()>(baz)) : bazFn(fun){}
Alan Birtles
la source