J'ai un wrapper pour un morceau de code hérité.
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
};
Dans ce code hérité, la fonction qui «duplique» un objet n'est pas thread-safe (lors de l'appel au même premier argument), elle n'est donc pas marquée const
dans le wrapper. Je suppose que les règles modernes suivantes: https://herbsutter.com/2013/01/01/video-you-dont-know-const-and-mutable/
Cela duplicate
ressemble à un bon moyen d'implémenter un constructeur de copie, à l'exception du détail qui ne l'est pas const
. Je ne peux donc pas le faire directement:
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;}
};
Alors, comment sortir de cette situation paradoxale?
(Disons aussi que ce legacy_duplicate
n'est pas thread-safe mais je sais que l'objet reste dans son état d'origine à sa sortie. Étant une fonction C, le comportement est seulement documenté mais n'a pas de concept de constance.)
Je peux penser à de nombreux scénarios possibles:
(1) Une possibilité est qu'il n'y a aucun moyen d'implémenter un constructeur de copie avec la sémantique habituelle. (Oui, je peux déplacer l'objet et ce n'est pas ce dont j'ai besoin.)
(2) D'un autre côté, la copie d'un objet est intrinsèquement non thread-safe dans le sens où la copie d'un type simple peut trouver la source dans un état semi-modifié, donc je peux simplement avancer et le faire peut-être,
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 même simplement déclarer duplicate
const et mentir sur la sécurité des threads dans tous les contextes. (Après tout, la fonction héritée ne se soucie pas, const
donc le compilateur ne se plaindra même pas.)
class A{
L* impl_;
A(A const& other) : L{other.duplicate()}{}
L* duplicate() const{L* ret; legacy_duplicate(impl_, &ret); return ret;}
};
(4) Enfin, je peux suivre la logique et créer un constructeur de copie qui prend un argument non 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;}
};
Il s'avère que cela fonctionne dans de nombreux contextes, car ces objets ne le sont généralement pas const
.
La question est, est-ce un itinéraire valable ou commun?
Je ne peux pas les nommer, mais je m'attends intuitivement à beaucoup de problèmes sur la route d'avoir un constructeur de copie non-const. Il ne sera probablement pas considéré comme un type de valeur en raison de cette subtilité.
(5) Enfin, bien que cela semble être une exagération et pourrait avoir un coût d'exécution élevé, je pourrais ajouter un 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;
};
Mais être forcé de faire cela ressemble à de la pessimisation et agrandit la classe. Je ne suis pas sûr. Je penche actuellement vers (4) ou (5) ou une combinaison des deux.
—— MODIFIER
Une autre option:
(6) Oubliez tout le non-sens de la fonction membre en double et appelez simplement à legacy_duplicate
partir du constructeur et déclarez que le constructeur de copie n'est pas thread-safe. (Et si nécessaire, faites une autre version thread-safe du type, A_mt
)
class A{
L* impl_;
A(A const& other){legacy_duplicate(other.impl_, &impl_);}
};
EDIT 2
Cela pourrait être un bon modèle pour ce que fait la fonction héritée. Notez qu'en touchant l'entrée, l'appel n'est pas thread-safe par rapport à la valeur représentée par le premier argument.
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
ne peut pas être appelée avec le même premier argument à partir de deux threads différents.
const
vraiment ce que cela signifie. :-) Je ne penserais pas à deux fois avant de prendre un const&
dans mon ctor de copie tant que je ne modifie pas other
. Je pense toujours à la sécurité des threads comme quelque chose que l'on ajoute en plus de tout ce qui doit être accessible à partir de plusieurs threads, via l'encapsulation, et j'attends vraiment les réponses avec impatience.
L
lequel est modifié en créant une nouvelleL
instance? Sinon, pourquoi pensez-vous que cette opération n'est pas thread-safe?