Inicialize vários membros da classe constante usando uma chamada de função C ++

50

Se eu tiver duas variáveis ​​de membros constantes diferentes, que precisam ser inicializadas com base na mesma chamada de função, existe uma maneira de fazer isso sem chamar a função duas vezes?

Por exemplo, uma classe de fração em que numerador e denominador são constantes.

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

Isso resulta em perda de tempo, pois a função GCD é chamada duas vezes. Você também pode definir um novo membro da classe,gcd_a_b , e primeiro atribuir a saída do gcd à da lista de inicializadores, mas isso levaria à perda de memória.

Em geral, existe uma maneira de fazer isso sem chamadas de função desperdiçadas ou memória? Você pode criar variáveis ​​temporárias em uma lista de inicializadores? Obrigado.

Qq0
fonte
5
Você tem provas de que "a função GCD é chamada duas vezes"? É mencionado duas vezes, mas isso não é a mesma coisa que um código de emissão do compilador que o chama duas vezes. Um compilador pode deduzir que é uma função pura e reutilizar seu valor na segunda menção.
Eric Towers
6
@ EricTowers: Sim, algumas vezes os compiladores podem solucionar o problema na prática em alguns casos. Mas somente se eles puderem ver a definição (ou alguma anotação em um objeto), caso contrário, não há como provar que é pura. Você deve compilar com a otimização do tempo do link ativada, mas nem todo mundo faz. E a função pode estar em uma biblioteca. Ou considere o caso de uma função que faz têm efeitos colaterais, e chamando-o exatamente uma vez é uma questão de correção?
Peter Cordes
@EricTowers Ponto interessante. Na verdade, tentei checá-lo colocando uma instrução de impressão dentro da função GCD, mas agora percebo que isso impediria que ela fosse uma função pura.
Qq0 5/04
@ Qq0: Você pode verificar olhando o compilador gerado asm, por exemplo, usando o Godbolt compiler explorer com gcc ou clang -O3. Mas, provavelmente, para qualquer implementação de teste simples, ele realmente alinharia a chamada de função. Se você usar __attribute__((const))ou purificar o protótipo sem fornecer uma definição visível, ele deve permitir que o GCC ou o clang façam a eliminação da subexpressão comum (CSE) entre as duas chamadas com o mesmo argumento. Observe que a resposta de Drew funciona mesmo para funções não puras, por isso é muito melhor e você deve usá-la sempre que a função não estiver alinhada.
Peter Cordes
Geralmente, é melhor evitar variáveis ​​de membro const não estáticas. Uma das poucas áreas em que const tudo nem sempre se aplica. Por exemplo, você não pode atribuir objetos de classe. Você pode colocar_back em um vetor, mas apenas enquanto o limite de capacidade não for redimensionado.
doug

Respostas:

66

Em geral, existe uma maneira de fazer isso sem chamadas de função desperdiçadas ou memória?

Sim. Isso pode ser feito com um construtor de delegação , introduzido no C ++ 11.

Um construtor de delegação é uma maneira muito eficiente de adquirir valores temporários necessários para a construção antes que qualquer variável membro seja inicializada.

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;
};
Drew Dormann
fonte
Fora de interesse, a sobrecarga de chamar outro construtor seria significativa?
Qq0 5/04
11
@ Qq0 Você pode observar aqui que não há custos indiretos com as otimizações modestas ativadas.
Drew Dormann
2
@ Qq0: O C ++ foi desenvolvido com base nos modernos compiladores de otimização. Eles podem alinhar trivialmente essa delegação, especialmente se você a tornar visível na definição de classe (na .h), mesmo que a definição real do construtor não seja visível para embutir. isto é, a gcd()chamada seria incorporada em cada site de chamada do construtor e deixaria apenas um callpara o construtor privado de 3 operandos.
Peter Cordes
10

Os vars de membros são inicializados pela ordem em que são declarados na decleração de classe, portanto, você pode fazer o seguinte (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;
}

Não há necessidade de chamar outros construtores ou até mesmo fazê-los.

asmmo
fonte
6
ok, que funciona especificamente para o GCD, mas muitos outros casos de uso provavelmente não podem derivar a segunda const dos argumentos e da primeira. E, como está escrito, isso tem uma divisão extra, que é outra desvantagem versus ideal, que o compilador pode não otimizar. O GCD pode custar apenas uma divisão, portanto isso pode ser tão ruim quanto chamar o GCD duas vezes. (Supondo que a divisão domine o custo de outras operações, como costuma acontecer nas CPUs modernas.)
Peter Cordes
@PeterCordes, mas a outra solução possui uma chamada de função extra e aloca mais memória de instruções.
asmmo 5/04
11
Você está falando do construtor delegador de Drew? Obviamente, isso pode incluir a Fraction(a,b,gcd(a,b))delegação no chamador, levando a um custo total menor. Esse inlining é mais fácil para o compilador do que desfazer a divisão extra nisso. Eu não tentei no godbolt.org, mas você poderia, se estiver curioso. Use gcc ou clang -O3como uma compilação normal usaria. (C ++ é desenhado em torno do pressuposto de um compilador otimizado moderno, por conseguinte, apresenta como constexpr)
Pedro Cordes
-3

@Drew Dormann deu uma solução semelhante ao que eu tinha em mente. Como o OP nunca menciona não poder modificar o ctor, isso pode ser chamado com Fraction f {a, b, gcd(a, b)}:

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

Só assim, não há segunda chamada para uma função, construtora ou outra, portanto não há perda de tempo. E não é uma perda de memória, já que um temporário teria que ser criado de qualquer maneira, então você também pode fazer bom uso dele. Também evita uma divisão extra.

um cidadão preocupado
fonte
3
Sua edição faz nem responder à pergunta. Agora você está exigindo que o chamador passe um terceiro argumento? Sua versão original usando atribuição dentro do corpo do construtor não funciona const, mas pelo menos funciona para outros tipos. E que divisão extra você "também" está evitando? Você quer dizer a resposta do asmmo?
Peter Cordes
11
Ok, removi meu voto negativo agora que você explicou seu ponto de vista. Mas isso parece obviamente terrível e requer que você alinhe manualmente parte do trabalho do construtor em todos os chamadores. Isso é o oposto de DRY (não se repita) e do encapsulamento da responsabilidade / classe da classe. A maioria das pessoas não consideraria isso uma solução aceitável. Dado que existe uma maneira do C ++ 11 de fazer isso de forma limpa, ninguém deve fazer isso a menos que esteja preso a uma versão mais antiga do C ++, e a classe tenha muito poucas chamadas para esse construtor.
Peter Cordes
2
@aconcernedcitizen: Não quero dizer por razões de desempenho, mas por razões de qualidade de código. Do seu jeito, se você alguma vez mudou como essa classe funcionava internamente, teria que encontrar todas as chamadas para o construtor e alterar esse terceiro argumento. Esse extra ,gcd(foo, bar)é um código extra que poderia e, portanto, deveria ser fatorado em todos os sites de chamada na fonte . Esse é um problema de manutenção / legibilidade, não de desempenho. O compilador provavelmente o incluirá no momento da compilação, o que você deseja para o desempenho.
Peter Cordes
11
@ PeterCordes Você está certo, agora vejo que minha mente estava concentrada na solução e desconsiderei todo o resto. De qualquer maneira, a resposta permanece, mesmo que apenas para envergonhar. Sempre que tiver dúvidas, saberei onde procurar.
um cidadão preocupado
11
Considere também o caso de Fraction f( x+y, a+b ); Para escrever do seu jeito, você teria que escrever BadFraction f( x+y, a+b, gcd(x+y, a+b) );ou usar tmp vars. Ou, pior ainda, e se você quiser escrever Fraction f( foo(x), bar(y) );- então você precisaria que o site de chamada declarasse alguns tmp vars para manter os valores de retorno ou chame essas funções novamente e espere que o compilador as expire, o que estamos evitando. Deseja depurar o caso de um chamador misturando os argumentos para gcdque não seja realmente o GCD dos dois primeiros argumentos passados ​​para o construtor? Não? Então não torne possível esse bug.
Peter Cordes