È questo un noto errore di C ++ 11 per i cicli?


89

Immaginiamo di avere una struttura per contenere 3 doppie con alcune funzioni membro:

struct Vector {
  double x, y, z;
  // ...
  Vector &negate() {
    x = -x; y = -y; z = -z;
    return *this;
  }
  Vector &normalize() {
     double s = 1./sqrt(x*x+y*y+z*z);
     x *= s; y *= s; z *= s;
     return *this;
  }
  // ...
};

Questo è un po 'artificioso per semplicità, ma sono sicuro che sei d'accordo sul fatto che un codice simile sia disponibile. I metodi ti consentono di concatenare comodamente, ad esempio:

Vector v = ...;
v.normalize().negate();

O anche:

Vector v = Vector{1., 2., 3.}.normalize().negate();

Ora, se fornissimo le funzioni begin () e end (), potremmo usare il nostro Vector in un nuovo stile del ciclo for, ad esempio per eseguire il ciclo sulle 3 coordinate x, yez (senza dubbio puoi costruire esempi più "utili" sostituendo Vector con es. String):

Vector v = ...;
for (double x : v) { ... }

Possiamo anche fare:

Vector v = ...;
for (double x : v.normalize().negate()) { ... }

e anche:

for (double x : Vector{1., 2., 3.}) { ... }

Tuttavia, il seguente (mi sembra) non funziona:

for (double x : Vector{1., 2., 3.}.normalize()) { ... }

Anche se sembra una combinazione logica dei due usi precedenti, penso che quest'ultimo utilizzo crei un riferimento penzolante mentre i due precedenti sono completamente a posto.

  • È corretto e ampiamente apprezzato?
  • Quale parte di quanto sopra è la parte "cattiva", che dovrebbe essere evitata?
  • Il linguaggio sarebbe migliorato cambiando la definizione del ciclo for basato su intervallo in modo tale che i provvisori costruiti nell'espressione for esistano per la durata del ciclo?

Per qualche ragione ricordo una domanda molto simile che mi è stata fatta prima, ma ho dimenticato come si chiamava.
Pubby

Lo considero un difetto del linguaggio. La vita dei provvisori non è estesa all'intero corpo del ciclo for, ma solo per la configurazione del ciclo for. Non è solo la sintassi dell'intervallo che soffre, ma anche la sintassi classica. A mio parere la vita dei provvisori nell'istruzione init dovrebbe estendersi per l'intera durata del ciclo.
edA-qa mort-ora-y

1
@ edA-qamort-ora-y: tendo ad essere d'accordo sul fatto che ci sia un leggero difetto del linguaggio in agguato qui, ma penso che sia specificamente il fatto che l'estensione della durata si verifica implicitamente ogni volta che leghi direttamente un temporaneo a un riferimento, ma non in alcun altra situazione - questa sembra una soluzione incompleta al problema sottostante delle vite temporanee, anche se questo non vuol dire che sia ovvio quale sarebbe una soluzione migliore. Forse una sintassi esplicita di "estensione della vita" durante la costruzione del temporaneo, che lo fa durare fino alla fine del blocco corrente - cosa ne pensi?
ndkrempel

@ edA-qamort-ora-y: ... questo equivale a legare il temporaneo a un riferimento, ma ha il vantaggio di essere più esplicito per il lettore che si sta verificando 'estensione della vita', inline (in un'espressione , piuttosto che richiedere una dichiarazione separata) e non richiedere di nominare il temporaneo.
ndkrempel

Risposte:


64

È corretto e ampiamente apprezzato?

Sì, la tua comprensione delle cose è corretta.

Quale parte di quanto sopra è la parte "cattiva", che dovrebbe essere evitata?

La parte negativa è prendere un riferimento a un valore l a un valore temporaneo restituito da una funzione e associarlo a un riferimento a un valore r. È altrettanto brutto come questo:

auto &&t = Vector{1., 2., 3.}.normalize();

La Vector{1., 2., 3.}durata del temporaneo non può essere estesa perché il compilatore non ha idea che il valore restituito da vi faccia normalizeriferimento.

Il linguaggio sarebbe migliorato cambiando la definizione del ciclo for basato su intervallo in modo tale che i provvisori costruiti nell'espressione for esistano per la durata del ciclo?

Ciò sarebbe altamente incoerente con il funzionamento del C ++.

Impedirebbe certi trucchi fatti da persone che usano espressioni concatenate su provvisori o vari metodi di valutazione pigra per le espressioni? Sì. Ma richiederebbe anche codice del compilatore per casi speciali, oltre a creare confusione sul motivo per cui non funziona con altri costrutti di espressione.

Una soluzione molto più ragionevole potrebbe essere un modo per informare il compilatore che il valore restituito di una funzione è sempre un riferimento a this, e quindi se il valore restituito è associato a un costrutto di estensione temporanea, estenderebbe il valore temporaneo corretto. Questa è una soluzione a livello di lingua.

Attualmente (se il compilatore lo supporta), puoi farlo in modo che normalize non possa essere chiamato temporaneamente:

struct Vector {
  double x, y, z;
  // ...
  Vector &normalize() & {
     double s = 1./sqrt(x*x+y*y+z*z);
     x *= s; y *= s; z *= s;
     return *this;
  }
  Vector &normalize() && = delete;
};

Ciò causerà Vector{1., 2., 3.}.normalize()un errore di compilazione, mentre v.normalize()funzionerà bene. Ovviamente non sarai in grado di fare cose corrette come questa:

Vector t = Vector{1., 2., 3.}.normalize();

Ma non sarai nemmeno in grado di fare cose sbagliate.

In alternativa, come suggerito nei commenti, puoi fare in modo che la versione di riferimento rvalue restituisca un valore anziché un riferimento:

struct Vector {
  double x, y, z;
  // ...
  Vector &normalize() & {
     double s = 1./sqrt(x*x+y*y+z*z);
     x *= s; y *= s; z *= s;
     return *this;
  }
  Vector normalize() && {
     Vector ret = *this;
     ret.normalize();
     return ret;
  }
};

Se Vectorfosse un tipo con risorse effettive da spostare, potresti Vector ret = std::move(*this);invece usare . L'ottimizzazione del valore di ritorno denominato lo rende ragionevolmente ottimale in termini di prestazioni.


1
La cosa che potrebbe renderlo più un "gotcha" è che il nuovo ciclo for nasconde sintatticamente il fatto che l'associazione di riferimento sta succedendo sotto le coperte - cioè è molto meno sfacciata dei tuoi esempi "altrettanto cattivi" sopra. Ecco perché sembrava plausibile suggerire la regola di estensione della durata extra, solo per il nuovo ciclo for.
ndkrempel

1
@ndkrempel: Sì, ma se hai intenzione di proporre una funzionalità linguistica per risolvere questo problema (e quindi devi aspettare almeno fino al 2017), preferirei che fosse più completo, qualcosa che potrebbe risolvere il problema dell'estensione temporanea ovunque .
Nicol Bolas

3
+1. Nell'ultimo approccio, invece di deletefornire un'operazione alternativa che restituisca un rvalue: Vector normalize() && { normalize(); return std::move(*this); }(credo che la chiamata normalizeall'interno della funzione invierà all'overload lvalue, ma qualcuno dovrebbe controllarlo :)
David Rodríguez - dribeas

3
Non ho mai visto questo &/ &&qualificazione dei metodi. È da C ++ 11 o è un'estensione del compilatore proprietaria (forse diffusa). Offre possibilità interessanti.
Christian Rau

1
@ChristianRau: È nuovo in C ++ 11 ed è analogo alle qualifiche "const" e "volatile" di C ++ 03 delle funzioni membro non statiche, in quanto qualifica "questo" in un certo senso. g ++ 4.7.0 non lo supporta tuttavia.
ndkrempel

25

for (double x: Vector {1., 2., 3.}. normalize ()) {...}

Questa non è una limitazione della lingua, ma un problema con il tuo codice. L'espressione Vector{1., 2., 3.}crea una temporanea, ma la normalizefunzione restituisce un lvalue-riferimento . Poiché l'espressione è un lvalue , il compilatore presume che l'oggetto sarà attivo , ma poiché si tratta di un riferimento a un elemento temporaneo, l'oggetto muore dopo la valutazione dell'espressione completa, quindi rimane un riferimento sospeso.

Ora, se si modifica la progettazione per restituire un nuovo oggetto in base al valore anziché un riferimento all'oggetto corrente, non ci sarebbero problemi e il codice funzionerebbe come previsto.


1
Un constriferimento estenderebbe la durata dell'oggetto in questo caso?
David Stone

5
Il che spezzerebbe la semantica chiaramente desiderata di normalize()come funzione mutante su un oggetto esistente. Così la domanda. Il fatto che un temporaneo abbia una "durata di vita estesa" quando viene utilizzato per lo scopo specifico di un'iterazione, e non altrimenti, penso sia un errore che crea confusione.
Andy Ross

2
@ AndyRoss: Perché? Qualsiasi associazione temporanea a un riferimento di valore r (o const&) ha la sua durata estesa.
Nicol Bolas

2
@ndkrempel: Ancora, non è una limitazione del range-based per ciclo, lo stesso problema sarebbe venuto se si associa a un riferimento: Vector & r = Vector{1.,2.,3.}.normalize();. Il tuo progetto ha questa limitazione, e questo significa che o sei disposto a restituire per valore (il che potrebbe avere senso in molte circostanze, e ancora di più con i riferimenti rvalue e lo spostamento ), oppure devi gestire il problema al posto di call: crea una variabile appropriata, quindi usala nel ciclo for. Si noti inoltre che l'espressione Vector v = Vector{1., 2., 3.}.normalize().negate();crea due oggetti ...
David Rodríguez - dribeas

1
@ DavidRodríguez-dribeas: il problema con l'associazione a const-reference è questo: T const& f(T const&);va benissimo. T const& t = f(T());è completamente a posto. E poi, in un'altra TU lo scopri T const& f(T const& t) { return t; }e piangi ... Se operator+opera sui valori è più sicuro ; quindi il compilatore può ottimizzare la copia (Vuoi velocità? Passa per valori), ma questo è un bonus. L'unico legame dei provvisori che consentirei è il legame ai riferimenti ai valori r, ma le funzioni dovrebbero quindi restituire valori per sicurezza e fare affidamento su Copia elisione / spostamento semantica.
Matthieu M.

4

IMHO, il secondo esempio è già difettoso. Il fatto che gli operatori di modifica restituiscano *thisè conveniente nel modo in cui hai accennato: consente il concatenamento di modificatori. Esso può essere utilizzato per la semplice consegna sul risultato della modifica, ma facendo questo è soggetto ad errori, perché può facilmente essere trascurato. Se vedo qualcosa di simile

Vector v{1., 2., 3.};
auto foo = somefunction1(v, 17);
auto bar = somefunction2(true, v, 2, foo);
auto baz = somefunction3(bar.quun(v), 93.2, v.qwarv(foo));

Non sospetterei automaticamente che le funzioni vengano modificate vcome effetto collaterale. Certo, potrebbero , ma sarebbe fonte di confusione. Quindi, se dovessi scrivere qualcosa del genere, mi assicurerei che vrimanga costante. Per il tuo esempio, aggiungerei funzioni gratuite

auto normalized(Vector v) -> Vector {return v.normalize();}
auto negated(Vector v) -> Vector {return v.negate();}

e poi scrivi i loop

for( double x : negated(normalized(v)) ) { ... }

e

for( double x : normalized(Vector{1., 2., 3}) ) { ... }

Questo è IMO più leggibile ed è più sicuro. Naturalmente, richiede una copia extra, tuttavia per i dati allocati nell'heap ciò potrebbe essere probabilmente fatto in un'operazione di spostamento C ++ 11 economica.


Grazie. Come al solito, ci sono molte scelte. Una situazione in cui il tuo suggerimento potrebbe non essere praticabile è se Vector è un array (non allocato nell'heap) di 1000 doppi, ad esempio. Un compromesso tra efficienza, facilità d'uso e sicurezza.
ndkrempel

2
Sì, ma raramente è utile avere strutture con dimensione> ≈100 nello stack, comunque.
sinistra intorno
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.