Inizializza più membri di classe costanti utilizzando una chiamata di funzione C ++


50

Se ho due diverse variabili dei membri costanti, che devono entrambe essere inizializzate in base alla stessa chiamata di funzione, c'è un modo per farlo senza chiamare la funzione due volte?

Ad esempio, una classe di frazione in cui numeratore e denominatore sono costanti.

int gcd(int a, int b); // Greatest Common Divisor
class Fraction {
public:
    // Lets say we want to initialize to a reduced fraction
    Fraction(int a, int b) : numerator(a/gcd(a,b)), denominator(b/gcd(a,b))
    {

    }
private:
    const int numerator, denominator;
};

Ciò comporta una perdita di tempo, poiché la funzione GCD viene chiamata due volte. È inoltre possibile definire un nuovo membro della classe gcd_a_be assegnare prima l'output di gcd a quello nell'elenco di inizializzatori, ma ciò comporterebbe uno spreco di memoria.

In generale, c'è un modo per farlo senza sprecare chiamate di funzione o memoria? Puoi forse creare variabili temporanee in un elenco di inizializzatori? Grazie.


5
Hai la prova che "la funzione GCD è chiamata due volte"? È menzionato due volte, ma non è la stessa cosa di un compilatore che emette codice che lo chiama due volte. Un compilatore può dedurre che si tratta di una funzione pura e riutilizzare il suo valore alla seconda menzione.
Eric Towers,

6
@EricTowers: Sì, in alcuni casi i compilatori possono aggirare il problema in pratica. Ma solo se riescono a vedere la definizione (o qualche annotazione in un oggetto), altrimenti nessun modo per dimostrarlo è puro. Si dovrebbe compilare con l'ottimizzazione di collegamento in tempo attivato, ma non tutti lo fa. E la funzione potrebbe essere in una libreria. Oppure si consideri il caso di una funzione che fa avere effetti collaterali, e chiamandolo esattamente una volta è una questione di correttezza?
Peter Cordes,

@EricTowers Punto interessante. In realtà ho provato a controllarlo inserendo un'istruzione print all'interno della funzione GCD, ma ora mi rendo conto che ciò gli impedirebbe di essere una funzione pura.
Qq0

@ Qq0: Puoi controllare guardando il compilatore generato asm, ad esempio usando l'esploratore del compilatore Godbolt con gcc o clang -O3. Ma probabilmente per qualsiasi semplice implementazione di test in realtà verrebbe incorporata la chiamata di funzione. Se si utilizza __attribute__((const))o puro sul prototipo senza fornire una definizione visibile, dovrebbe consentire a GCC o clang di eseguire l'eliminazione della sottoespressione comune (CSE) tra le due chiamate con lo stesso argomento. Nota che la risposta di Drew funziona anche per funzioni non pure, quindi è molto meglio e dovresti usarla ogni volta che la funzione potrebbe non essere in linea.
Peter Cordes,

Generalmente, è meglio evitare le variabili costanti non statiche. Una delle poche aree in cui const tutto non si applica spesso. Ad esempio non è possibile assegnare oggetti di classe. Puoi emplace_back in un vettore ma solo fino a quando il limite di capacità non avvia un ridimensionamento.
Doug

Risposte:


66

In generale, c'è un modo per farlo senza sprecare chiamate di funzione o memoria?

Sì. Questo può essere fatto con un costruttore delegato , introdotto in C ++ 11.

Un costruttore delegante è un modo molto efficiente per acquisire i valori temporanei necessari per la costruzione prima di inizializzare qualsiasi variabile membro.

int gcd(int a, int b); // Greatest Common Divisor
class Fraction {
public:
    // Call gcd ONCE, and forward the result to another constructor.
    Fraction(int a, int b) : Fraction(a,b,gcd(a,b))
    {
    }
private:
    // This constructor is private, as it is an
    // implementation detail and not part of the public interface.
    Fraction(int a, int b, int g_c_d) : numerator(a/g_c_d), denominator(b/g_c_d)
    {
    }
    const int numerator, denominator;
};

Per interesse, il sovraccarico di chiamare un altro costruttore sarebbe significativo?
Qq0

1
@ Qq0 Qui puoi osservare che non ci sono costi generali con ottimizzazioni modeste abilitate.
Ha disegnato Dormann il

2
@ Qq0: C ++ è progettato attorno a moderni compilatori di ottimizzazione. Possono insinuare banalmente questa delega, specialmente se la rendi visibile nella definizione della classe (nella .h), anche se la definizione del costruttore reale non è visibile per l'inline. vale a dire che la gcd()chiamata verrebbe incorporata in ciascun sito del costruttore e lascerebbe solo un callal costruttore privato a 3 operandi.
Peter Cordes,

10

I membri membri vengono inizializzati dall'ordine in cui vengono dichiarati nella declerazione di classe, quindi è possibile effettuare le seguenti operazioni (matematicamente)

#include <iostream>
int gcd(int a, int b){return 2;}; // Greatest Common Divisor of (4, 6) just to test
class Fraction {
public:
    // Lets say we want to initialize to a reduced fraction
    Fraction(int a, int b) : numerator{a/gcd(a,b)}, denominator(b/(a/numerator))
    {

    }
//private:
    const int numerator, denominator;//make sure that they are in this order
};
//Test
int main(){
    Fraction f{4,6};
    std::cout << f.numerator << " / " << f.denominator;
}

Non c'è bisogno di chiamare altri costruttori o addirittura di farli.


6
ok che funziona specificamente per GCD, ma molti altri casi d'uso probabilmente non possono derivare la seconda const dagli arg e dalla prima. E come scritto questo ha una divisione extra che è un altro aspetto negativo rispetto all'ideale che il compilatore potrebbe non ottimizzare. GCD potrebbe costare solo una divisione, quindi potrebbe essere quasi brutto come chiamare GCD due volte. (Supponendo che questa divisione domini il costo di altre operazioni, come spesso accade sulle CPU moderne.)
Peter Cordes,

@PeterCordes ma l'altra soluzione ha una chiamata di funzione aggiuntiva e alloca più memoria di istruzioni.
asmmo

1
Stai parlando del costruttore delegato di Drew? Ciò può ovviamente incorporare la Fraction(a,b,gcd(a,b))delega nel chiamante, portando a un costo totale inferiore. Questo inline è più facile da fare per il compilatore che per annullare la divisione aggiuntiva in questo. Non l'ho provato su godbolt.org ma potresti farlo se sei curioso. Usa gcc o clang -O3come farebbe una normale build. (C ++ è progettato attorno al presupposto di un moderno compilatore di ottimizzazione, quindi caratteristiche come constexpr)
Peter Cordes

-3

@Drew Dormann ha dato una soluzione simile a quello che avevo in mente. Poiché OP non menziona mai la possibilità di modificare il ctor, questo può essere chiamato con Fraction f {a, b, gcd(a, b)}:

Fraction(int a, int b, int tmp): numerator {a/tmp}, denominator {b/tmp}
{
}

Solo in questo modo non c'è una seconda chiamata a una funzione, un costruttore o altro, quindi non è tempo perso. E non è uno spreco di memoria poiché un temporaneo dovrebbe essere creato comunque, quindi potresti anche farne buon uso. Evita anche una divisione extra.


3
La modifica non fa nemmeno rispondere alla domanda. Ora stai richiedendo al chiamante di passare un terzo argomento? La versione originale che utilizza l'assegnazione all'interno del corpo del costruttore non funziona const, ma funziona almeno per altri tipi. E quale divisione extra stai "evitando" di evitare? Intendi la risposta di asmmo?
Peter Cordes,

1
Ok, ho rimosso il mio voto negativo ora che hai spiegato il tuo punto. Ma questo sembra abbastanza ovviamente terribile, e richiede di incorporare manualmente parte del lavoro del costruttore in ogni chiamante. Questo è l'opposto di DRY (non ripeterti) e l'incapsulamento della responsabilità della classe / degli interni. Molte persone non la considererebbero una soluzione accettabile. Dato che esiste un modo C ++ 11 per farlo in modo pulito, nessuno dovrebbe mai farlo a meno che forse non siano bloccati con una versione C ++ precedente e la classe abbia pochissime chiamate a questo costruttore.
Peter Cordes,

2
@aconcernedcitizen: non intendo per motivi di prestazioni, intendo per motivi di qualità del codice. A modo tuo, se mai cambiassi il modo in cui questa classe funzionava internamente, dovresti andare a trovare tutte le chiamate al costruttore e cambiare quel terzo argomento. Quel extra ,gcd(foo, bar)è un codice extra che potrebbe e quindi dovrebbe essere preso in considerazione da ogni call nel sorgente . Questo è un problema di manutenibilità / leggibilità, non prestazioni. Molto probabilmente il compilatore lo incorporerà al momento della compilazione, che si desidera per le prestazioni.
Peter Cordes,

1
@PeterCordes Hai ragione, ora vedo che la mia mente è stata fissata sulla soluzione e ho ignorato tutto il resto. In entrambi i casi, la risposta rimane, anche solo per vergogna. Ogni volta che avrò dei dubbi, saprò dove cercare.
un cittadino interessato il

1
Considera anche il caso di Fraction f( x+y, a+b ); scriverlo a modo tuo, dovresti scrivere BadFraction f( x+y, a+b, gcd(x+y, a+b) );o usare tmp vars. O peggio ancora, e se volessi scrivere Fraction f( foo(x), bar(y) );- allora avresti bisogno del sito di chiamata per dichiarare alcuni tmp vars per contenere i valori di ritorno, o chiamare di nuovo quelle funzioni e sperare che il compilatore le CSE, cosa che stiamo evitando. Vuoi eseguire il debug del caso in cui un chiamante mischia gli arg in gcdmodo che non sia effettivamente il GCD dei primi 2 argomenti passati al costruttore? No? Quindi non rendere possibile quel bug.
Peter Cordes,
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.