Temos um aplicativo muito maior que depende da sobrecarga de modelo de matrizes char e const char. No gcc 7.5, clang e visual studio, o código abaixo imprime "NÃO CONST" para todos os casos. No entanto, para o gcc 8.1 e posterior, a saída é mostrada abaixo:
#include <iostream>
class MyClass
{
public:
template <size_t N>
MyClass(const char (&value)[N])
{
std::cout << "CONST " << value << '\n';
}
template <size_t N>
MyClass(char (&value)[N])
{
std::cout << "NON-CONST " << value << '\n';
}
};
MyClass test_1()
{
char buf[30] = "test_1";
return buf;
}
MyClass test_2()
{
char buf[30] = "test_2";
return {buf};
}
void test_3()
{
char buf[30] = "test_3";
MyClass x{buf};
}
void test_4()
{
char buf[30] = "test_4";
MyClass x(buf);
}
void test_5()
{
char buf[30] = "test_5";
MyClass x = buf;
}
int main()
{
test_1();
test_2();
test_3();
test_4();
test_5();
}
A saída gcc 8 e 9 (do godbolt) é:
CONST test_1
NON-CONST test_2
NON-CONST test_3
NON-CONST test_4
NON-CONST test_5
Isso me parece um bug do compilador, mas acho que poderia ser outro problema relacionado a uma alteração de idioma. Alguém sabe definitivamente?
Respostas:
Quando você retorna uma expressão de identificação simples de uma função (que designou um objeto local da função), o compilador é obrigado a executar a resolução de sobrecarga duas vezes. Primeiro, trata-o como se fosse um valor, e não um valor. Somente se a primeira resolução de sobrecarga falhar, ela será executada novamente com o objeto como um valor l.
Se adicionarmos uma sobrecarga de rvalue,
a saída se tornará
e isso estaria correto. O que não está correto é o comportamento do GCC como você o vê. Considera a primeira resolução de sobrecarga um sucesso. Isso ocorre porque uma referência const lvalue pode se ligar a um rvalue. No entanto, ele ignora o texto "ou se o tipo do primeiro parâmetro do construtor selecionado não for uma referência rvalue ao tipo do objeto" . De acordo com isso, ele deve descartar o resultado da primeira resolução de sobrecarga e fazê-lo novamente.
Bem, essa é a situação até C ++ 17 de qualquer maneira. O rascunho padrão atual diz algo diferente.
O texto de até C ++ 17 foi removido. Portanto, é um bug de viagem no tempo. O GCC implementa o comportamento do C ++ 20, mas o faz mesmo quando o padrão é C ++ 17.
fonte
clang++
a implementação do C ++ 20 também não está correta, uma vez que usa a versão NON-CONST em todos os casos no código original.Há um debate sobre se esse é ou não um "comportamento intuitivo" nos comentários, então pensei em dar uma facada no raciocínio por trás desse comportamento.
Há uma palestra bastante agradável que foi dada no CPPCON que torna isso um pouco mais claro para mim { conversa , slides }. Basicamente, o que implica uma função que usa uma referência não-const? Que o objeto de entrada deve ser de leitura / gravação . Ainda mais forte, implica que pretendo modificar esse objeto, essa função tem efeitos colaterais . Um const ref implica somente leitura , e rvalue ref significa que eu posso pegar os recursos . Se você
test_1()
acabar chamando oNON-CONST
construtor, isso significa que pretendo modificar esse objeto, mesmo depois de concluído, ele não existe mais,que (acho) seria um bug (estou pensando em um caso em que uma referência é vinculada durante a inicialização depende se o argumento passado é const ou não).O que é um pouco mais preocupante para mim é a sutileza introduzida por
test_2()
. Aqui, a inicialização da lista de cópias está ocorrendo em vez das regras relacionadas a [class.copy.elision] citadas acima. Agora você está realmente dizendo para retornar um objeto do tipo MyClass como se eu o tivesse inicializadobuf
, para que oNON-CONST
comportamento seja invocado. Eu sempre pensei nas listas init como maneiras de ser mais concisas, mas aqui os aparelhos fazem uma diferença semântica significativa. Isso importaria mais se os construtores tivessemMyClass
um grande número de argumentos. Então, diga que você deseja criarbuf
, modifique-o e retorne-o com o grande número de argumentos, invocando oCONST
comportamento. Por exemplo, digamos que você tenha os construtores:E teste:
Godbolt nos diz que obtemos
NON-CONST
comportamento, emboraCONST
seja provavelmente o que queremos (depois de você ter bebido o auxílio legal na semântica de argumentos de função). Mas agora a inicialização da lista de cópias não faz o que gostaríamos. O teste a seguir melhora meu argumento:Agora, para obter a semântica adequada com a inicialização da lista de cópias, o buffer precisa ser "recuperado" no final. Eu acho que se o objetivo fosse que esse objeto inicializasse outro
MyClass
objeto, apenas o uso doNON-CONST
comportamento na lista de cópias de retorno seria bom se o construtor de movimentação / cópia invocasse qualquer que seja o comportamento apropriado, mas isso está começando a parecer bastante delicado.fonte