Eu tenho um wrapper para algum pedaço de código legado.
class A{
L* impl_; // the legacy object has to be in the heap, could be also unique_ptr
A(A const&) = delete;
L* duplicate(){L* ret; legacy_duplicate(impl_, &L); return ret;}
... // proper resource management here
};
Nesse código legado, a função que "duplica" um objeto não é segura para threads (ao chamar o mesmo primeiro argumento), portanto, não está marcada const
no wrapper. Eu acho que seguindo as regras modernas: https://herbsutter.com/2013/01/01/video-you-dont-know-const-and-mutable/
Esta duplicate
parece ser uma boa maneira de implementar um construtor de cópia, exceto para o detalhe que não é const
. Portanto, não posso fazer isso diretamente:
class A{
L* impl_; // the legacy object has to be in the heap
A(A const& other) : L{other.duplicate()}{} // error calling a non-const function
L* duplicate(){L* ret; legacy_duplicate(impl_, &ret); return ret;}
};
Então, qual é a saída dessa situação paradoxal?
(Digamos também que legacy_duplicate
não é seguro para threads, mas eu sei que deixa o objeto no estado original quando ele sai. Sendo uma função C, o comportamento é apenas documentado, mas não tem conceito de constância.)
Eu posso pensar em muitos cenários possíveis:
(1) Uma possibilidade é que não há como implementar um construtor de cópias com a semântica usual. (Sim, eu posso mover o objeto e não é disso que preciso.)
(2) Por outro lado, copiar um objeto é inerentemente não seguro para threads, no sentido de que copiar um tipo simples pode encontrar a fonte em um estado semi-modificado, para que eu possa ir adiante e fazer isso talvez,
class A{
L* impl_;
A(A const& other) : L{const_cast<A&>(other).duplicate()}{} // error calling a non-const function
L* duplicate(){L* ret; legacy_duplicate(impl_, &ret); return ret;}
};
(3) ou até mesmo declarar duplicate
const e mentir sobre segurança de threads em todos os contextos. (Afinal, a função legada não se importa, const
então o compilador nem se queixará.)
class A{
L* impl_;
A(A const& other) : L{other.duplicate()}{}
L* duplicate() const{L* ret; legacy_duplicate(impl_, &ret); return ret;}
};
(4) Finalmente, posso seguir a lógica e criar um construtor de cópias que usa um argumento não-const .
class A{
L* impl_;
A(A const&) = delete;
A(A& other) : L{other.duplicate()}{}
L* duplicate(){L* ret; legacy_duplicate(impl_, &ret); return ret;}
};
Acontece que isso funciona em muitos contextos, porque esses objetos geralmente não são const
.
A questão é: esta é uma rota válida ou comum?
Não posso nomeá-los, mas, intuitivamente, espero muitos problemas no caminho de ter um construtor de cópias não-const. Provavelmente não se qualificará como um tipo de valor por causa dessa sutileza.
(5) Finalmente, embora isso pareça um exagero e possa ter um alto custo de tempo de execução, eu poderia adicionar um mutex:
class A{
L* impl_;
A(A const& other) : L{other.duplicate_locked()}{}
L* duplicate(){
L* ret; legacy_duplicate(impl_, &ret); return ret;
}
L* duplicate_locked() const{
std::lock_guard<std::mutex> lk(mut);
L* ret; legacy_duplicate(impl_, &ret); return ret;
}
mutable std::mutex mut;
};
Mas ser forçado a fazer isso parece pessimização e aumenta a classe. Não tenho certeza. Atualmente, estou inclinado a (4) ou (5) ou a uma combinação de ambos.
—— EDITAR
Outra opção:
(6) Esqueça toda a falta de sentido da função de membro duplicado e simplesmente chame legacy_duplicate
o construtor e declare que o construtor de cópia não é seguro para threads. (E, se necessário, faça outra versão com segurança para threads do tipo A_mt
)
class A{
L* impl_;
A(A const& other){legacy_duplicate(other.impl_, &impl_);}
};
EDIT 2
Este poderia ser um bom modelo para o que a função legada faz. Observe que, ao tocar na entrada, a chamada não é segura com relação ao valor representado pelo primeiro argumento.
void legacy_duplicate(L* in, L** out){
*out = new L{};
char tmp = in[0];
in[0] = tmp;
std::memcpy(*out, in, sizeof *in); return;
}
legacy_duplicate
não pode ser chamada com o mesmo primeiro argumento de dois threads diferentes.
const
realmente significa. :-) Eu não pensaria duas vezes em tirar um const&
cópia no meu copiador, desde que não o modifique other
. Eu sempre penso na segurança de threads como algo que se acrescenta sobre o que precisa ser acessado de vários threads, via encapsulamento, e estou realmente ansioso pelas respostas.
L
qual é modificado criando uma novaL
instância? Caso contrário, por que você acredita que esta operação não é segura para threads?