Membro di riferimento aggregato e durata temporanea

Dato questo esempio di codice, quali sono le regole relative alla durata della stringa temporanea passata a S

 struct S { // [1] S(const std::string& str) : str_{str} {} // [2] S(S&& other) : str_{std::move(other).str} {} const std::string& str_; }; S a{"foo"}; // direct-initialization auto b = S{"bar"}; // copy-initialization with rvalue std::string foobar{"foobar"}; auto c = S{foobar}; // copy-initialization with lvalue const std::string& baz = "baz"; auto d = S{baz}; // copy-initialization with lvalue-ref to temporary 

Secondo lo standard:

N4140 12.2 p5.1 (rimosso in N4296)

Un vincolo temporaneo a un membro di riferimento nel ctor-initializer di un costruttore (12.6.2) persiste finché il costruttore non si chiude.

N4296 12.6.2 p8

Un’espressione temporanea legata a un membro di riferimento in un inizializzatore di mem è mal formata.

Quindi avere un costruttore definito dall’utente come [1] è definitivamente quello che vogliamo. Si suppone che sia malformato nell’ultimo C ++ 14 (o è vero?) Né gcc né clang ne hanno avvertito.
Cambia con l’inizializzazione degli aggregati diretti? In questo caso, mi sembra che la durata temporanea sia stata estesa.

Ora, riguardo all’inizializzazione della copia, il costruttore di movimento di default ei membri di riferimento affermano che [2] è generato implicitamente. Dato che la mossa potrebbe essere eliminata, la stessa regola si applica al costruttore di mosse generato implicitamente?

Quale di a, b, c, d ha un riferimento valido?

La durata degli oggetti temporanei associati ai riferimenti viene estesa, a meno che non ci sia un’eccezione specifica. Cioè, se non c’è una tale eccezione, allora la durata sarà estesa.

Da una bozza abbastanza recente, N4567:

Il secondo contesto [dove la durata è estesa] è quando un riferimento è legato a un temporaneo. Il temporaneo a cui il riferimento è vincolato o il temporaneo che è l’object completo di un sottoobject a cui il riferimento è associato persiste per la durata del riferimento eccetto:

  • (5.1) Un object temporaneo associato a un parametro di riferimento in una chiamata di funzione (5.2.2) persiste fino al completamento dell’espressione completa che contiene la chiamata.
  • (5.2) La durata di un vincolo temporaneo al valore restituito in una dichiarazione di ritorno di funzione (6.6.3) non è estesa; il temporaneo viene distrutto alla fine dell’espressione completa nella dichiarazione di reso.
  • (5.3) Un vincolo temporaneo a un riferimento in un nuovo inizializzatore (5.3.4) persiste fino al completamento dell’espressione completa contenente il nuovo inizializzatore.

L’unica modifica significativa a C ++ 11 è, come detto dall’OP, che in C ++ 11 c’era un’eccezione aggiuntiva per i membri di dati dei tipi di riferimento (da N3337):

  • Un vincolo temporaneo a un membro di riferimento nel ctor-initializer di un costruttore (12.6.2) persiste finché il costruttore non si chiude.

Ciò è stato rimosso in CWG 1696 (post-C ++ 14) e il binding di oggetti temporanei per fare riferimento ai membri dei dati tramite il mem-inizializzatore è ora mal formato.


Riguardo agli esempi nel PO:

 struct S { const std::string& str_; }; S a{"foo"}; // direct-initialization 

Questo crea uno std::string temporaneo e inizializza il membro str_ data con esso. S a{"foo"} utilizza l’inizializzazione di aggregazione, quindi non è coinvolto alcun inizializzatore di mem. Nessuna delle eccezioni per le estensioni di durata si applica, quindi la durata di tale temporaneo è estesa alla durata del membro di dati di riferimento str_ .


 auto b = S{"bar"}; // copy-initialization with rvalue 

Prima della copia obbligatoria elision con C ++ 17: Formalmente, creiamo una std::string temporanea std::string , inizializza una S temporanea legando la std::string temporanea std::string al membro str_ reference. Quindi, spostiamo quella S temporanea in b . Questo “copierà” il riferimento, che non prolungherà la durata dello std::string temporaneo. Tuttavia, le implementazioni porteranno il passaggio dalla S temporanea alla b . Tuttavia, ciò non deve influire sulla durata della std::string temporanea std::string . È ansible osservare questo nel seguente programma:

 #include  #define PRINT_FUNC() { std::cout << __PRETTY_FUNCTION__ << "\n"; } struct loud { loud() PRINT_FUNC() loud(loud const&) PRINT_FUNC() loud(loud&&) PRINT_FUNC() ~loud() PRINT_FUNC() }; struct aggr { loud const& l; ~aggr() PRINT_FUNC() }; int main() { auto x = aggr{loud{}}; std::cout << "end of main\n"; (void)x; } 

Dimostrazione dal vivo

Nota che il distruttore di loud è chiamato prima della "fine del main", mentre x vive fino a dopo quella traccia. Formalmente, il loud temporaneo viene distrutto alla fine della piena espressione che lo ha creato.

Il comportamento non cambia se il costruttore di move di aggr è definito dall'utente.

Con copy-elision obbligatorio in C ++ 17: identifichiamo l'object sul rhs S{"bar"} con l'object sul lhs b . Ciò fa sì che la durata del temporaneo sia estesa alla durata di b . Vedi CWG 1697 .


Per i restanti due esempi, il costruttore di movimento, se chiamato, copia semplicemente il riferimento. Il costruttore di movimenti (di S ) può essere eliminato, ovviamente, ma ciò non è osservabile poiché copia solo il riferimento.