Considere o código de exemplo de brinquedo abaixo. Ele tem uma estrutura de dados muito grande para copiar MyDataStructure
e uma pequena classe iteradora MyDataIterator
que o chamador pode instanciar para iterar sobre os dados na estrutura.
Na versão de brinquedo do código, a estrutura de dados contém apenas 10 inteiros (1000-1009) e a WORKING CODE
seção main()
itera sobre eles corretamente, como esperado.
Entretanto, a segunda BROKEN CODE
seção ( ) main()
não itera sobre eles corretamente; ela invoca um comportamento indefinido porque chama GetDataStructureByValue()
em vez de GetDataStructureByReference()
, o que significa que a MyDataStructure
referência iter
armazenada em sua _ds
variável membro privada se torna uma referência pendente antes da primeira iteração do loop.
Este é um problema um tanto insidioso, porque em código não-de-brinquedo nem sempre é fácil dizer/lembrar pelo nome de uma função se ela está retornando por valor ou por referência, mas o primeiro caso resulta em um bug de tempo de execução não óbvio, enquanto o último caso funciona bem.
Minha pergunta é: existe alguma maneira razoável de fazer um compilador C++ moderno dar erro ou pelo menos avisar sobre o caso problemático? Suspeito que não, mas estou perguntando mesmo assim porque me sentiria mais seguro se instâncias desse problema pudessem ser sinalizadas automaticamente para mim de alguma forma.
O código-fonte do programa de brinquedo está abaixo, seguido por um exemplo de sua saída no meu Mac executando o XCode:
#include <stdio.h>
class MyDataStructure
{
public:
MyDataStructure()
{
printf("MyDataStructure CTOR %p\n", this);
for (int i=0; i<10; i++) _data[i] = i+1000;
}
~MyDataStructure()
{
printf("MyDataStructure DTOR %p\n", this);
for (int i=0; i<10; i++) _data[i] = -1; // just to make the symptoms more obvious
}
bool IsPositionValid(int pos) const {return ((pos >= 0)&&(pos < 10));}
int GetValueAt(int pos) const {return _data[pos];}
private:
int _data[10];
};
const MyDataStructure & GetDataStructureByReference()
{
static MyDataStructure _ds;
return _ds;
}
MyDataStructure GetDataStructureByValue()
{
MyDataStructure _ds;
return _ds;
}
class MyDataIterator
{
public:
MyDataIterator(const MyDataStructure & ds) : _ds(ds), _idx(0) {/* empty */}
bool HasValue() const {return _ds.IsPositionValid(_idx);}
int GetValue() const {return _ds.GetValueAt(_idx);}
void operator++(int) {_idx++;}
const MyDataStructure & GetDataStructure() const {return _ds;}
private:
const MyDataStructure & _ds;
int _idx;
};
int main(int, char **)
{
{
// The following line of code is okay; it counts from 1000 to 10009, as expected
printf("WORKING CODE:\n");
for (MyDataIterator iter(GetDataStructureByReference()); iter.HasValue(); iter++) printf("iter returned %i from %p\n", iter.GetValue(), &iter.GetDataStructure());
}
{
// The following line of code is buggy because, the MyDataStructure object that (iter) keeps a reference to
// gets destroyed before the first iteration of the loop.
printf("\nBROKEN CODE:\n");
for (MyDataIterator iter(GetDataStructureByValue()); iter.HasValue(); iter++) printf("iter returned %i from %p\n", iter.GetValue(), &iter.GetDataStructure());
}
printf("\nEND OF PROGRAM\n");
return 0;
}
... exemplo de saída:
$ ./a.out
WORKING CODE:
MyDataStructure CTOR 0x1015c4000
iter returned 1000 from 0x1015c4000
iter returned 1001 from 0x1015c4000
iter returned 1002 from 0x1015c4000
iter returned 1003 from 0x1015c4000
iter returned 1004 from 0x1015c4000
iter returned 1005 from 0x1015c4000
iter returned 1006 from 0x1015c4000
iter returned 1007 from 0x1015c4000
iter returned 1008 from 0x1015c4000
iter returned 1009 from 0x1015c4000
BROKEN CODE:
MyDataStructure CTOR 0x7ff7be93d198
MyDataStructure DTOR 0x7ff7be93d198
iter returned -1 from 0x7ff7be93d198
iter returned -1 from 0x7ff7be93d198
iter returned -1 from 0x7ff7be93d198
iter returned -1 from 0x7ff7be93d198
iter returned -1 from 0x7ff7be93d198
iter returned -1 from 0x7ff7be93d198
iter returned -1 from 0x7ff7be93d198
iter returned -1 from 0x7ff7be93d198
iter returned -1 from 0x7ff7be93d198
iter returned -1 from 0x7ff7be93d198
END OF PROGRAM
MyDataStructure DTOR 0x1015c4000
Uma coisa fácil de fazer é simplesmente impedir que o iterador possa ser criado com um objeto temporário. Se você adicionar outro construtor
MyDataIterator
com a seguinte assinaturaEntão esse construtor será o escolhido pela resolução de sobrecarga, pois é uma melhor combinação para rvalues. Como ele é deletado, o compilador emitirá um erro porque você não pode chamar um construtor deletado. Você pode vê-lo "funcionando" (gerando um erro) aqui neste exemplo ao vivo .