Kiedy informacje o typach są cofane w C ++?

92

Właśnie oglądałem Stephan T. Lavavej w CppCon 2018programie „Class Template Argument Deduction”, gdzie w pewnym momencie mimochodem mówi:

W C ++ informacje prawie nigdy nie płyną wstecz ... Musiałem powiedzieć „prawie”, ponieważ jest jeden lub dwa przypadki, prawdopodobnie więcej, ale bardzo niewiele .

Pomimo próby ustalenia, do których przypadków może się odnosić, nie mogłem nic wymyślić. Stąd pytanie:

W jakich przypadkach standard C ++ 17 nakazuje, aby informacje typu były propagowane wstecz?

Massimiliano
źródło
wzorce dopasowujące częściowe przypisania specjalizacji i destrukturyzacji.
v.oddou

Odpowiedzi:

80

Oto co najmniej jeden przypadek:

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

jeśli to zrobisz foo f; int x = f; double y = f;, informacje o typie będą płynąć „wstecz”, aby dowiedzieć się, co Tjest w środku operator T.

Możesz użyć tego w bardziej zaawansowany sposób:

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

więc teraz mogę to zrobić

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

i to działa.

Oczywiście, dlaczego po prostu nie zrobić {1,2,3}? Cóż, {1,2,3}to nie jest wyrażenie.

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

co, co prawda, wymaga nieco więcej magii: przykład na żywo . (Muszę dokonać deduce return, wykonać sprawdzenie SFINAE F, następnie uczynić F przyjaznym dla SFINAE i muszę zablokować std :: initializer_list w operatorze deduce_return_t T.)

Yakk - Adam Nevraumont
źródło
Bardzo interesująca odpowiedź i nauczyłem się nowej sztuczki, więc bardzo dziękuję! Musiałem dodać wytyczne dotyczące dedukcji szablonu, aby skompilować Twój przykład , ale poza tym działa jak urok!
Massimiliano
5
&&Kwalifikator na operator T()to świetny akcent; pomaga uniknąć słabej interakcji z auto, powodując błąd kompilacji, jeśli autozostanie tu niewłaściwie użyty.
Justin
1
To bardzo imponujące, czy możesz wskazać mi jakieś odniesienie / rozmowę do pomysłu z przykładu? a może jest oryginalny :) ...
llllllllll
3
@lili Jaki pomysł? Liczę 5: Używanie operatora T do wywnioskowania typów zwracanych? Używanie tagów do przekazywania wywnioskowanego typu do lambdy? Używasz operatorów konwersji do rozwijania własnej konstrukcji obiektu umieszczania? Podłączasz wszystkie 4?
Yakk - Adam Nevraumont
1
@lili Tha „bardziej zaawansowany sposób” to, jak powiedziałem, tylko cztery pomysły sklejone ze sobą. Zrobiłem klejenie w locie dla tego postu, ale z pewnością widziałem wiele par lub nawet trojaczków tych używanych razem. Jest to zbiór dość mało znanych technik (jak narzeka tootsie), ale nic nowego.
Yakk - Adam Nevraumont
31

Stephan T. Lavavej wyjaśnił sprawę, o której mówił w tweecie :

Przypadek, o którym myślałem, polega na tym, że możesz wziąć adres przeciążonej / opartej na szablonie funkcji, a jeśli jest ona używana do inicjalizacji zmiennej określonego typu, uściśli to, którą z nich chcesz. (Jest lista tego, co ujednoznacznia).

możemy zobaczyć przykłady tego na stronie cppreference pod adresem Adres przeciążonej funkcji , z wyjątkiem kilku poniżej:

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 dodaje :

Nie ogranicza się to również do inicjowania konkretnego typu. Mógłby również wywnioskować tylko z liczby argumentów

i przedstawia ten przykład na żywo :

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

które omówię tutaj trochę więcej .

Shafik Yaghmour
źródło
4
Możemy to również opisać jako: przypadki, w których typ wyrażenia zależy od kontekstu?
MM
20

Uważam, że w statycznym rzutowaniu przeciążonych funkcji przepływ idzie w przeciwnym kierunku, jak w zwykłej rozdzielczości przeciążenia. Myślę, że jeden z nich jest odwrotny.

jbapple
źródło
7
Uważam, że to prawda. Dzieje się tak, gdy przekazujesz nazwę funkcji do typu wskaźnika funkcji; informacje o typie płyną z kontekstu wyrażenia (typ, do którego przypisujesz / konstruujesz / itp.) wstecz do nazwy funkcji, aby określić, które przeciążenie jest wybrane.
Yakk - Adam Nevraumont