Quand les informations de type circulent-elles en arrière en C ++?

92

Je viens de regarder Stephan T. Lavavej parler CppCon 2018de "Déduction d'argument de modèle de classe", où, à un moment donné, il dit incidemment:

Dans le type C ++, les informations ne circulent presque jamais en arrière ... J'ai dû dire «presque» car il y a un ou deux cas, peut-être plus mais très peu .

Bien que j'aie essayé de comprendre à quels cas il pouvait faire référence, je ne pouvais rien trouver. D'où la question:

Dans quels cas la norme C ++ 17 exige que les informations de type se propagent à l'envers?

Massimiliano
la source
correspondance de modèles de spécialisation partielle et affectations de déstructuration.
v.oddou

Réponses:

80

Voici au moins un cas:

struct foo {
  template<class T>
  operator T() const {
    std::cout << sizeof(T) << "\n";
    return {};
  }
};

si vous le faites foo f; int x = f; double y = f;, les informations de type circuleront "en arrière" pour comprendre ce qu'il Ty a dedans operator T.

Vous pouvez l'utiliser de manière plus avancée:

template<class T>
struct tag_t {using type=T;};

template<class F>
struct deduce_return_t {
  F f;
  template<class T>
  operator T()&&{ return std::forward<F>(f)(tag_t<T>{}); }
};
template<class F>
deduce_return_t(F&&)->deduce_return_t<F>;

template<class...Args>
auto construct_from( Args&&... args ) {
  return deduce_return_t{ [&](auto ret){
    using R=typename decltype(ret)::type;
    return R{ std::forward<Args>(args)... };
  }};
}

alors maintenant je peux faire

std::vector<int> v = construct_from( 1, 2, 3 );

et il fonctionne.

Bien sûr, pourquoi ne pas le faire {1,2,3}? Eh bien, ce {1,2,3}n'est pas une expression.

std::vector<std::vector<int>> v;
v.emplace_back( construct_from(1,2,3) );

qui, certes, nécessitent un peu plus de magie: exemple en direct . (Je dois faire en sorte que le retour de déduction fasse une vérification SFINAE de F, puis rendre le F compatible avec SFINAE, et je dois bloquer std :: initializer_list dans l'opérateur deduce_return_t T.)

Yakk - Adam Nevraumont
la source
Réponse très intéressante, et j'ai appris une nouvelle astuce alors merci beaucoup! J'ai dû ajouter un guide de déduction de modèle pour que votre exemple soit compilé , mais à part cela, cela fonctionne comme un charme!
Massimiliano
5
Le &&qualificatif sur le operator T()est une excellente touche; cela permet d'éviter la mauvaise interaction avec autoen provoquant une erreur de compilation s'il autoest mal utilisé ici.
Justin
1
C'est très impressionnant, pourriez-vous m'indiquer une référence / parler de l'idée dans l'exemple? ou peut-être que c'est original :) ...
llllllllll
3
@lili Quelle idée? Je compte 5: Utiliser l'opérateur T pour déduire les types de retour? Utiliser des balises pour passer le type déduit à un lambda? Vous utilisez des opérateurs de conversion pour créer vous-même des objets de placement? Connecter les 4?
Yakk - Adam Nevraumont
1
L'exemple de @lili Tha "plus avancé" est, comme je l'ai dit, juste 4 idées collées ensemble. J'ai fait le collage à la volée pour cet article, mais j'ai certainement vu de nombreuses paires ou même des triplets de ceux utilisés ensemble. C'est un tas de techniques raisonnablement obscures (comme se plaint tootsie), mais rien de nouveau.
Yakk - Adam Nevraumont
31

Stephan T.Lavavej a expliqué le cas dont il parlait dans un tweet :

Le cas auquel je pensais est celui où vous pouvez prendre l'adresse d'une fonction surchargée / basée sur un modèle et si elle est utilisée pour initialiser une variable d'un type spécifique, cela clarifiera celle que vous voulez. (Il y a une liste de ce qui clarifie.)

nous pouvons voir des exemples de cela à partir de la page cppreference sur l'adresse de la fonction surchargée , j'en ai excepté quelques-uns ci-dessous:

int f(int) { return 1; } 
int f(double) { return 2; }   

void g( int(&f1)(int), int(*f2)(double) ) {}

int main(){
    g(f, f); // selects int f(int) for the 1st argument
             // and int f(double) for the second

     auto foo = []() -> int (*)(int) {
        return f; // selects int f(int)
    }; 

    auto p = static_cast<int(*)(int)>(f); // selects int f(int)
}

Michael Park ajoute :

Cela ne se limite pas non plus à l'initialisation d'un type concret. Il pourrait également déduire uniquement du nombre d'arguments

et fournit cet exemple en direct :

void overload(int, int) {}
void overload(int, int, int) {}

template <typename T1, typename T2,
          typename A1, typename A2>
void f(void (*)(T1, T2), A1&&, A2&&) {}

template <typename T1, typename T2, typename T3,
          typename A1, typename A2, typename A3>
void f(void (*)(T1, T2, T3), A1&&, A2&&, A3&&) {}

int main () {
  f(&overload, 1, 2);
}

que j'élabore un peu plus ici .

Shafik Yaghmour
la source
4
On pourrait aussi décrire cela comme: des cas où le type d'une expression dépend du contexte?
MM
20

Je crois que dans la diffusion statique de fonctions surchargées, le flux va dans la direction opposée comme dans la résolution de surcharge habituelle. Donc l'un de ceux-ci est à l'envers, je suppose.

jbapple
la source
7
Je pense que c'est correct. Et c'est lorsque vous passez un nom de fonction à un type de pointeur de fonction; les informations de type s'écoulent du contexte de l'expression (le type que vous attribuez à / construisez / etc) vers l'arrière dans le nom de la fonction pour déterminer quelle surcharge est choisie.
Yakk - Adam Nevraumont