Tão confuso com a inicialização da lista cpp e a divergência do compilador.
Isso aconteceu enquanto eu estava revisando alguns recursos do cpp.
U ({ arg1, arg2, ... }) (11)
cppreference #copy-list-initialization case 11
diz
expressão de conversão funcional ou outras invocações de construtor, onde uma lista de inicializadores entre chaves é usada no lugar de um argumento do construtor. A inicialização da lista de cópias inicializa o parâmetro do construtor (observe que o tipo U neste exemplo não é o tipo que está sendo inicializado por lista;
U's constructor's parameter
é)
Então tentei o seguinte código para ver qual construtor o compilador Cpp escolherá:
#include <string>
#include <vector>
#include <map>
#include <tuple>
#include <iostream>
using namespace std;
struct A {
static int id;
A(int integer, float sci, string str, vector<int> vnum, pair<int, int> intP, tuple<int, string, float> isfTuple) {
}
A(pair<int, float> ifP) { // this also allows {} for default construction
}
A(const A &) = delete;
A(A &&) = delete;
~A() {
cout << "Destructed: " << ++id << endl;
}
};
int main(){
A b({1, 1, "str", {1, 2}, {1, 1}, {1, "str", 1}}); // works on MinGW
// MinGW version: x86_64-14.2.0-release-win32-seh-ucrt-rt_v12-rev2
// MSVC error: 'A::A(A &&)': attempting to reference a deleted function
A partial({1, 1.0}); // works both on MinGW and MSVC with ISO C++20 Standard (/std:c++20)
}
No caso em questão A b(...)
, fiquei confuso sobre o motivo pelo qual o compilador funciona mesmo com o construtor de cópia A(const A &) = delete;
e o construtor de movimento A(A&&)
excluídos. Então, pesquisei sobre a conversão de construtores , a elisão de cópia e o rascunho de CPP em inicializadores, mas fiquei ainda mais confuso. Tentei compilar com MSVC e obtive o erro:
'A::A(A &&)': tentando referenciar uma função excluída
Então o Compiler Explorer obteve sucesso.
Então, o que está acontecendo? Bug no compilador MinGW ou MSVC? Ou este é um comportamento indefinido (mas o caso 11 parece estar bem definido)? Por que o MinGW e o Compiler Explorer tiveram sucesso?
Meus scripts de construção do GCC (usando vscode tasks.json):
g++ -Wall --std=gnu++20 -g ${file} -o ${fileDirname}\\${fileBasenameNoExtension}
Plataforma: Windows 11, é claro.
Conjunto de ferramentas do Visual Studio:Visual Studio 2022 (v143)
Padrão de linguagem do Visual Studio:ISO C++20 Standard (/std:c++20)
Resultado do Compiler Explorer
Editar:
Meus testes posteriores mostram que tanto o MSVC quanto o MinGW usam os mesmos construtores para cada caso se nenhum construtor for removido -- Result . O resultado do stdio permanece o mesmo. Há apenas uma divergência no comportamento de verificação do compilador (não sei qual é realmente) e o resultado da compilação é o mesmo, novamente, se nenhum construtor for removido. E sob -std=gnu++11, onde a eliminação de cópia não está disponível até o C++17, tanto o MSVC quanto o MinGW mantêm a mesma saída -- nenhum construtor de cópia/movimentação foi usado.
Então A b...
, na verdade, o caso de uso 1 é uma sintaxe de inicialização direta, em vez do caso 11, o que faz a sintaxe do caso 1 entrar em conflito com o caso de teste, A b...
ou ele está, na verdade, usando outra coisa?
Editar:
Parece que a elisão de cópia sempre acontece desde o C++ 11 durante a inicialização, mas algumas divergências acontecem no processamento de pré-valor na atribuição e na expressão de retorno entre compiladores. Acredito que a melhor prática é evitar aproveitar as vantagens mínimas do efeito colateral dos construtores.
Editar:
Acho que @Jarod42 está certo.
Para tornar seu código portátil entre GCC , Clang e MSVC , você pode forçar a inicialização direta usando inicialização uniforme com parênteses em vez de depender da inicialização por chaves em uma conversão funcional:
Opção 1: Use parênteses e construa subobjetos manualmente
Isso evita a resolução ambígua
{}
e evita o fallback incorreto do MSVC para o construtor de movimento.Opção 2: Fornecer uma fábrica auxiliar do tipo agregado (se necessário)
Isso garante que os argumentos sejam passados corretamente e que a elisão seja aplicada sem invocar construtores excluídos.
Mas por que
A b({ ... });
funciona no GCC/MinGW, mas falha no MSVC?Esta linha:
A b({1, 1, "str", {1, 2}, {1, 1}, {1, "str", 1}});
é uma inicialização de lista de cópias. De acordo com C++20:
O compilador procura um construtor que
A
corresponda a todos os valores dentro das chaves.GCC e Clang resolvem isso corretamente para o construtor
A(int, float, string, vector<int>, pair<int, int>, tuple<int, string, float>)
Como o objeto está sendo construído diretamente, a eliminação de cópia é garantida (desde C++17), portanto nenhum construtor de cópia ou movimentação é chamado, mesmo que sejam excluídos.
Então o GCC aceita isso.
Por que o MSVC rejeita isso?
O MSVC parece tentar incorretamente criar um temporário
A
e depois movê-lo parab
. Como o construtor de movimentação foi excluído, isso resulta em um erro de compilação.Esse comportamento não está em conformidade com o padrão C++20, que exige a eliminação de cópias em cenários de inicialização direta como este. Portanto, trata-se de um problema de compilador no MSVC.
Por que
A partial({1, 1.0});
funciona?A (pair<int, float>)
Não é necessário nenhum construtor de movimentação ou cópia, por isso ele compila bem tanto no GCC quanto no MSVC.
Resumo
GCC e Clang se comportam corretamente conforme C++20: eles executam inicialização direta com eliminação de cópia obrigatória.
O MSVC tenta usar incorretamente o construtor de movimento, apesar da elisão ser necessária.
Este é um problema do compilador no MSVC, não um comportamento indefinido.
{..}
não tem tipo.Em
A partial({1, 1.0});
,temos que procurar um construtor de sobrecarga de
A
, o melhor construtor viável éA(pair<int, float>)
então{1, 1.0}
usado para construir o par.Da mesma forma, para
A b({1, 1, "str", {1, 2}, {1, 1}, {1, "str", 1}});
, o melhor construtor viável é (o excluído )A(A&&)
.Selecionar esse construtor parece tornar o programa malformado para clang/msvc, mesmo que ele seja eliminado com elisão de cópia de garantia (c++17).
Fornecendo explicitamente
A
para{..}
(ou sejaA b(A{1, 1, "str", {1, 2}, {1, 1}, {1, "str", 1}});
) agradar a todos os compiladores Demo .