Quando le informazioni sul tipo fluiscono all'indietro in C ++?


92

Ho appena visto Stephan T. Lavavej parlare a CppCon 2018"Class Template Argument Deduction", dove a un certo punto dice per inciso:

In C ++ le informazioni di tipo non fluiscono quasi mai all'indietro ... Ho dovuto dire "quasi" perché ci sono uno o due casi, forse di più ma molto pochi .

Nonostante abbia cercato di capire a quali casi si riferisse, non sono riuscito a trovare nulla. Da qui la domanda:

In quali casi lo standard C ++ 17 impone che le informazioni sul tipo si propagano all'indietro?


pattern matching specializzazione parziale e incarichi di destrutturazione.
v.oddou

Risposte:


80

Ecco almeno un caso:

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

se lo fai foo f; int x = f; double y = f;, le informazioni sul tipo scorreranno "all'indietro" per capire cosa Tc'è dentro operator T.

Puoi usarlo in un modo più avanzato:

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

quindi ora posso farlo

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

e funziona.

Certo, perché non farlo e basta {1,2,3}? Beh, {1,2,3}non è un'espressione.

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

che, certamente, richiedono un po 'più di magia: esempio dal vivo . (Devo fare in modo che il deduce return esegua un controllo SFINAE di F, quindi rendi la F amichevole SFINAE, e devo bloccare std :: initializer_list nell'operatore deduce_return_t T.)


Risposta molto interessante, e ho imparato un nuovo trucco quindi grazie mille! Ho dovuto aggiungere una linea guida per la detrazione del modello per compilare il tuo esempio , ma a parte questo funziona a meraviglia!
Massimiliano

5
La &&qualificazione sul operator T()è un bel tocco; aiuta a evitare la scarsa interazione con autocausando un errore di compilazione se autoviene utilizzato in modo improprio qui.
Justin

1
È davvero impressionante, potresti indicarmi qualche riferimento / parlare dell'idea nell'esempio? o forse è originale :) ...
llllllllll

3
@lili Quale idea? Conto 5: utilizzare l'operatore T per dedurre i tipi di reso? Usando i tag per passare il tipo dedotto a un lambda? Utilizzi gli operatori di conversione per costruire oggetti di posizionamento personalizzati? Collegamento di tutti e 4?
Yakk - Adam Nevraumont

1
@lili L'esempio del "modo più avanzato" è, come ho detto, solo 4 o giù di lì idee incollate insieme. Ho fatto l'incollaggio al volo per questo post, ma sicuramente ho visto molte coppie o addirittura terzine di quelle usate insieme. È un mucchio di tecniche ragionevolmente oscure (come si lamenta Tootsie), ma niente di nuovo.
Yakk - Adam Nevraumont

31

Stephan T. Lavavej ha spiegato il caso di cui parlava in un tweet :

Il caso a cui stavo pensando è dove puoi prendere l'indirizzo di una funzione sovraccaricata / basata su modelli e se viene utilizzata per inizializzare una variabile di un tipo specifico, ciò disambigua quale vuoi. (C'è un elenco di ciò che disambigua.)

possiamo vedere esempi di questo dalla pagina cppreference su Indirizzo della funzione sovraccarica , ne ho esclusi alcuni di seguito:

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

Non si limita nemmeno all'inizializzazione di un tipo concreto. Potrebbe anche dedurre solo dal numero di argomenti

e fornisce questo esempio dal vivo :

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

che elaboro un po 'di più qui .


4
Potremmo anche descriverlo come: casi in cui il tipo di espressione dipende dal contesto?
MM

20

Credo che nel casting statico di funzioni sovraccariche il flusso vada nella direzione opposta come nella solita risoluzione del sovraccarico. Quindi uno di questi è al contrario, immagino.


7
Credo che questo sia corretto. Ed è quando si passa un nome di funzione a un tipo di puntatore a funzione; le informazioni sul tipo fluiscono dal contesto dell'espressione (il tipo a cui si sta assegnando / costruendo / ecc) all'indietro nel nome della funzione per determinare quale sovraccarico viene scelto.
Yakk - Adam Nevraumont
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.