web-dev-qa-db-fra.com

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

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

Car std::function est censé ne pas considérer la résolution de surcharge, comme je l'ai lu dans cet autre article .

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

J'ai lu les phases de traduction et templates sur cppreference, mais je ne peux penser à aucun raisonnement pour lequel 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?

17
TuRtoise

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 de 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 si cela prenait Args paramètres et renvoie quelque chose convertible enR (ou si R est void, il peut retourner n'importe quoi).

Pour ce faire, le constructeur approprié de std::function Doit ê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 baz repré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'argument de modèle pour déterminer ce qu'est ce paramètre, C++ n'a plus la capacité de déterminer la surcharge correcte dans l'ensemble de surcharge. Vous devez donc le spécifier directement.

13
Nicol Bolas

La résolution de surcharge se produit uniquement lorsque (a) vous appelez le nom d'une fonction/opérateur, ou (b) vous le transformez 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::function Prend 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 argument R(*)(Args...) (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 ensemble 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.

Ensuite:

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 que nous stockons dans fun, au lieu de passer fun un ensemble de surcharge directement (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 macro OVERLOADS_OF Est utile.

Vous pouvez aller plus loin et prendre en charge la conversion en pointeur de fonction compatible.

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)...); }; \
    } \
  }
7

Le problème ici est qu'il n'y a rien qui indique au compilateur comment effectuer la fonction de décomposition 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::function vous appelez son constructeur d'objet fonction qui a la forme

template< class F >
function( F f );

et comme il s'agit d'un modèle, il doit déduire le type de l'objet transmis. étant donné que baz est 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.

5
NathanOliver

Au moment où le compilateur décide quelle surcharge passer dans le std::function constructeur tout ce qu'il sait, c'est que le std::function le constructeur 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){}
1
Alan Birtles