Hay un debate sobre si esto es o no "comportamiento intuitivo" en los comentarios, por lo que pensé en apostar por el razonamiento detrás de este comportamiento.
Hubo una charla bastante agradable en CPPCON que me deja un poco más claro { talk , slides }. Básicamente, ¿qué implica una función que toma una referencia no constante? Que el objeto de entrada debe ser de lectura / escritura . Aún más fuerte, implica que tengo la intención de modificar este objeto, esta función tiene efectos secundarios . Una referencia constante implica solo lectura , y una referencia de valor significa que puedo tomar los recursos . Si test_1()
terminara llamando al NON-CONST
constructor, significaría que tengo la intención de modificar este objeto, aunque después de que haya terminado, ya no exista,lo cual (creo) sería un error (estoy pensando en un caso en el que la vinculación de una referencia durante la inicialización depende de si el argumento pasado es constante o no).
Lo que me preocupa un poco más es la sutileza introducida por test_2()
. Aquí, se está llevando a cabo la inicialización de la lista de copias en lugar de las reglas relativas a [class.copy.elision] citadas anteriormente. Ahora realmente está diciendo que devuelva un objeto de tipo MyClass como si lo hubiera inicializado buf
, por lo que NON-CONST
se invoca el comportamiento. Siempre he pensado en las listas de inicio como formas de ser más concisas, pero aquí las llaves hacen una diferencia semántica significativa. Esto importaría más si los constructores MyClass
tomaran una gran cantidad de argumentos. Luego, digamos que desea crear un buf
, modificarlo, luego devolverlo con la gran cantidad de argumentos, invocando el CONST
comportamiento. Por ejemplo, digamos que tienes los constructores:
template <size_t N>
MyClass(const char (&value)[N], int)
{
std::cout << "CONST int " << value << '\n';
}
template <size_t N>
MyClass(char (&value)[N], int)
{
std::cout << "NON-CONST int " << value << '\n';
}
Y prueba:
MyClass test_0() {
char buf[30] = "test_0";
return {buf,0};
}
Godbolt nos dice que obtenemos un NON-CONST
comportamiento, aunque CONST
probablemente sea lo que queremos (después de haber bebido la buena ayuda en la semántica de argumentos de función). Pero ahora la inicialización de la lista de copias no hace lo que nos gustaría. El siguiente tipo de prueba mejora mi punto:
MyClass test_0() {
char buf[30] = "test_0";
buf[0] = 'T';
const char (&bufR)[30]{buf};
return {bufR,0};
}
// OUTPUT: CONST int Test_0
Ahora para obtener la semántica adecuada con la inicialización de la lista de copias, el búfer debe ser "rebote" al final. Supongo que si el objetivo fuera que este objeto fuera inicializar algún otro MyClass
objeto, solo usar el NON-CONST
comportamiento en la lista de copias de retorno estaría bien si el constructor move / copy invocara el comportamiento apropiado, pero eso está comenzando a sonar bastante delicado.