Maneira linguística de distinguir dois construtores de zero-arg

41

Eu tenho uma classe como esta:

struct event_counts {
    uint64_t counts[MAX_COUNTERS];

    event_counts() : counts{} {}

    // more stuff

};

Normalmente, eu quero padrão (zero) inicializar a countsmatriz como mostrado.

Em locais selecionados identificados por criação de perfil, no entanto, eu gostaria de suprimir a inicialização do array, porque sei que o array está prestes a ser substituído, mas o compilador não é inteligente o suficiente para descobrir isso.

O que é uma maneira idiomática e eficiente de criar um construtor zero-arg "secundário"?

Atualmente, estou usando uma classe de tag uninit_tagque é passada como argumento fictício, assim:

struct uninit_tag{};

struct event_counts {
    uint64_t counts[MAX_COUNTERS];

    event_counts() : counts{} {}

    event_counts(uninit_tag) {}

    // more stuff

};

Então chamo o construtor no-init como event_counts c(uninit_tag{});quando quero suprimir a construção.

Estou aberto a soluções que não envolvam a criação de uma classe fictícia ou que são mais eficientes de alguma forma etc.

BeeOnRope
fonte
"porque eu sei que a matriz está prestes a ser substituída" Você tem 100% de certeza de que seu compilador não está fazendo essa otimização para você? Caso em questão: gcc.godbolt.org/z/bJnAuJ
Frank
6
@ Frank - Eu sinto que a resposta para sua pergunta está na segunda metade da frase que você citou? Ele não pertence à questão, mas várias coisas podem acontecer: (a) geralmente o compilador simplesmente não é forte o suficiente para eliminar os estoques mortos (b) às vezes, apenas um subconjunto dos elementos é substituído e isso derrota o otimização (mas apenas o mesmo subconjunto é lido posteriormente) (c) às vezes o compilador pode fazê-lo, mas é derrotado, por exemplo, porque o método não está embutido.
BeeOnRope 16/11/19
Você tem outros construtores em sua classe?
NathanOliver 16/11/19
11
@Frank - eh, seu caso em questão mostra que o gcc não elimina as lojas mortas? De fato, se você tivesse me feito adivinhar, eu teria pensado que o gcc acertaria esse caso muito simples, mas se ele falhar aqui, imagine um caso um pouco mais complicado!
BeeOnRope 16/11/19
11
@uneven_mark - sim, o gcc 9.2 faz isso em -O3 (mas essa otimização é incomum em comparação com -O2, IME), mas as versões anteriores não. Em geral, a eliminação de lojas mortas é uma coisa, mas é muito frágil e sujeita a todas as advertências usuais, como o compilador ser capaz de ver as lojas mortas ao mesmo tempo em que vê as lojas dominantes. Meu comentário foi mais para esclarecer o que Frank estava tentando dizer, porque ele disse "caso em questão: (link godbolt)", mas o link mostra as duas lojas sendo executadas (talvez esteja faltando alguma coisa).
BeeOnRope 16/11/19

Respostas:

33

A solução que você já tem está correta e é exatamente o que eu gostaria de ver se estivesse revendo seu código. É o mais eficiente possível, claro e conciso.

John Zwinck
fonte
11
A principal questão que tenho é se devo declarar um novo uninit_tagsabor em todos os lugares em que quero usar esse idioma. Eu esperava que já houvesse algo como esse tipo de indicador, talvez dentro std::.
BeeOnRope 16/11/19
9
Não há uma escolha óbvia na biblioteca padrão. Eu não definiria uma nova tag para cada classe em que quero esse recurso - definiria uma tag para todo o projeto no_inite a usaria em todas as minhas classes onde for necessário.
John Zwinck
2
Eu acho que a biblioteca padrão tem tags masculinas para diferenciar iteradores e essas coisas e os dois std::piecewise_construct_te std::in_place_t. Nenhum deles parece razoável para usar aqui. Talvez você queira definir um objeto global do seu tipo para usar sempre, para que você não precise de chaves em todas as chamadas de construtores. O STL faz isso com std::piecewise_constructfor std::piecewise_construct_t.
N314159
Não é tão eficiente quanto possível. Na convenção de chamada do AArch64, por exemplo, a tag deve ser alocada por pilha, com efeitos de bloqueio (também não pode ser chamada de cauda ...): godbolt.org/z/6mSsmq
TLW
11
@TLW Uma vez que você adicionar o corpo de construtores não há alocação de pilha, godbolt.org/z/vkCD65
R2RT
8

Se o corpo do construtor estiver vazio, ele poderá ser omitido ou padronizado:

struct event_counts {
    std::uint64_t counts[MAX_COUNTERS];
    event_counts() = default;
};

A inicialização padrão event_counts counts; deixará counts.countsnão inicializada (a inicialização padrão não é operacional aqui) e a inicialização do event_counts counts{}; valor valorizará a inicialização counts.counts, preenchendo-a efetivamente com zeros.

Evg
fonte
3
Mas, novamente, você deve se lembrar de usar a inicialização de valor e o OP deseja que seja seguro por padrão.
doc
@ doc, eu concordo. Esta não é a solução exata para o que o OP deseja. Mas essa inicialização imita os tipos internos. Pois int i;aceitamos que não seja inicializado com zero. Talvez devêssemos também aceitar que event_counts counts;não foi inicializado com zero e fazer event_counts counts{};nosso novo padrão.
Evg
6

Eu gosto da sua solução. Você também pode ter considerado struct e variável estática aninhada. Por exemplo:

struct event_counts {
    static constexpr struct uninit_tag {} uninit = uninit_tag();

    uint64_t counts[MAX_COUNTS];

    event_counts() : counts{} {}

    explicit event_counts(uninit_tag) {}

    // more stuff

};

Com a variável estática, a chamada do construtor não inicializado pode parecer mais conveniente:

event_counts e(event_counts::uninit);

Obviamente, você pode introduzir uma macro para salvar a digitação e torná-la mais um recurso sistemático

#define UNINIT_TAG static constexpr struct uninit_tag {} uninit = uninit_tag();

struct event_counts {
    UNINIT_TAG
}

struct other_counts {
    UNINIT_TAG
}
doc
fonte
3

Eu acho que um enum é uma escolha melhor do que uma classe de tag ou um bool. Você não precisa passar uma instância de uma estrutura e fica claro pelo chamador qual opção você está recebendo.

struct event_counts {
    enum Init { INIT, NO_INIT };
    uint64_t counts[MAX_COUNTERS];

    event_counts(Init init = INIT) {
        if (init == INIT) {
            std::fill(counts, counts + MAX_COUNTERS, 0);
        }
    }
};

Em seguida, a criação de instâncias fica assim:

event_counts e1{};
event_counts e2{event_counts::INIT};
event_counts e3{event_counts::NO_INIT};

Ou, para torná-lo mais parecido com a abordagem de classe de tag, use uma enumeração de valor único em vez da classe de tag:

struct event_counts {
    enum NoInit { NO_INIT };
    uint64_t counts[MAX_COUNTERS];

    event_counts() : counts{} {}
    explicit event_counts(NoInit) {}
};

Depois, existem apenas duas maneiras de criar uma instância:

event_counts e1{};
event_counts e2{event_counts::NO_INIT};
TimK
fonte
Eu concordo com você: enum são mais simples. Mas talvez você tenha esquecido esta linha:event_counts() : counts{} {}
azulada
@bluish, minha intenção não era inicializar countsincondicionalmente, mas apenas quando INITestiver definido.
TimK 21/11/19
@bluish Acho que o principal motivo para escolher uma classe de tag não é a simplicidade, mas para sinalizar que o objeto não inicializado é especial, ou seja, ele usa o recurso de otimização em vez da parte normal da interface da classe. Ambos boole enumsão decentes, mas temos que estar cientes que o uso de parâmetro em vez de sobrecarga tem um pouco diferente sombra semântica. No primeiro, você parametriza claramente um objeto, portanto, a postura inicializada / não inicializada se torna seu estado, enquanto a passagem de um objeto tag para o ctor é mais como pedir à classe que faça uma conversão. Portanto, a IMO não é uma questão de escolha sintática.
doc
@ TimK Mas o OP quer que o comportamento padrão seja a inicialização da matriz, então acho que sua solução para a questão deve incluir event_counts() : counts{} {}.
Azulado
@bluish Na minha sugestão original countsé inicializada por, a std::fillmenos que NO_INITseja solicitado. Adicionar o construtor padrão, como você sugere, criaria duas maneiras diferentes de fazer a inicialização padrão, o que não é uma boa idéia. Eu adicionei outra abordagem que evita o uso std::fill.
TimK 22/11/19
1

Você pode considerar uma inicialização em duas fases para sua classe:

struct event_counts {
    uint64_t counts[MAX_COUNTERS];

    event_counts() = default;

    void set_zero() {
       std::fill(std::begin(counts), std::end(counts), 0u);
    }
};

O construtor acima não inicializa a matriz para zero. Para definir os elementos da matriz como zero, é necessário chamar a função de membro set_zero()após a construção.

眠 り ネ ロ
fonte
7
Obrigado, considerei essa abordagem, mas quero algo que mantenha o padrão seguro - ou seja, zero por padrão, e apenas em alguns locais selecionados eu substituo o comportamento pelo inseguro.
BeeOnRope 16/11/19
3
Isso exigirá cuidados extras, exceto os usos que devem ser não inicializados. Portanto, é uma fonte extra de erros em relação à solução de OPs.
Walnut
O @BeeOnRope também pode fornecer std::functioncomo argumento construtor algo semelhante ao set_zeroargumento padrão. Você passaria uma função lambda se desejar uma matriz não inicializada.
doc
1

Eu faria assim:

struct event_counts {
    uint64_t counts[MAX_COUNTERS];

    event_counts() : counts{} {}

    event_counts(bool initCounts) {
        if (initCounts) {
            std::fill(counts, counts + MAX_COUNTERS, 0);
        }
    }
};

O compilador será inteligente o suficiente para pular todo o código quando você usar event_counts(false), e você poderá dizer exatamente o que quer dizer, em vez de tornar a interface da sua classe tão estranha.

Matt Timmermans
fonte
8
Você está certo quanto à eficiência, mas os parâmetros booleanos não produzem código de cliente legível. Quando você está lendo junto e vê a declaração event_counts(false), o que isso significa? Você não tem idéia sem voltar e olhar o nome do parâmetro. Melhor pelo menos usar uma enumeração ou, nesse caso, uma classe sentinela / tag, conforme mostrado na pergunta. Então, você obtém uma declaração mais parecida com event_counts(no_init), o que é óbvio para todos em seu significado.
Cody Gray
Eu acho que isso também é uma solução decente. Você pode descartar o controlador padrão e usar o valor padrão event_counts(bool initCountr = true).
doc
Além disso, o ctor deve ser explícito.
doc
infelizmente atualmente C ++ não suporta parâmetros nomeados, mas podemos usar boost::parametere chamada event_counts(initCounts = false)para facilitar a leitura
phuclv
11
Curiosamente, @doc, event_counts(bool initCounts = true)na verdade , é um construtor padrão, pois todos os parâmetros têm um valor padrão. O requisito é apenas que seja possível chamar sem especificar argumentos, event_counts ec;não se importa se é sem parâmetro ou usa valores padrão.
Justin Time - Restabelece Monica
1

Eu usaria uma subclasse apenas para economizar um pouco de digitação:

struct event_counts {
    uint64_t counts[MAX_COUNTERS];

    event_counts() : counts{} {}
    event_counts(uninit_tag) {}
};    

struct event_counts_no_init: event_counts {
    event_counts_no_init(): event_counts(uninit_tag{}) {}
};

Você pode se livrar da classe fictícia alterando o argumento do construtor não inicializado para boolou intou algo assim, pois ele não precisa mais ser mnemônico.

Você também pode trocar a herança e definir events_count_no_initcom um construtor padrão, como o Evg sugerido na resposta, e depois ter events_counta subclasse:

struct event_counts_no_init {
    uint64_t counts[MAX_COUNTERS];
    event_counts_no_init() = default;
};

struct event_counts: event_counts_no_init {
    event_counts(): event_counts_no_init{} {}
};
Ross Ridge
fonte
Essa é uma ideia interessante, mas também sinto que a introdução de um novo tipo causará atrito. Por exemplo, quando eu realmente quero um não inicializado event_counts, quero que seja do tipoevent_count , não event_count_uninitialized, então devo cortar direto na construção event_counts c = event_counts_no_init{};, o que acho que elimina a maior parte da economia na digitação.
BeeOnRope 17/11/19
@BeeOnRope Bem, para a maioria dos propósitos, um event_count_uninitialized objeto é um event_countobjeto. Esse é o ponto principal da herança, eles não são tipos completamente diferentes.
Ross cume
Concordou, mas o problema é com "para a maioria dos propósitos". Eles não são intercambiáveis ​​- por exemplo, se você tentar ver a atribuição ecua ecela funciona, mas não o contrário. Ou, se você usar funções de modelo, eles são tipos diferentes e terminam com instanciações diferentes, mesmo que o comportamento acabe sendo idêntico (e às vezes não será, por exemplo, com membros estáticos do modelo). Especialmente com o uso pesado autodisso, pode definitivamente surgir e ser confuso: eu não gostaria que a maneira como um objeto fosse inicializado permanentemente refletida em seu tipo.
BeeOnRope 17/11/19