У мене є обгортка для якогось фрагмента спадкового коду.
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
};
У цьому застарілому коді функція, що "копіює" об'єкт, не є безпечною для потоків (при виклику одного і того ж першого аргументу), тому не позначена const
в обгортці. Я думаю, дотримуючись сучасних правил: https://herbsutter.com/2013/01/01/video-you-dont-know-const-and-mutable/
Це duplicate
виглядає як хороший спосіб реалізації конструктора копій, за винятком деталей, які це не так const
. Тому я не можу зробити це безпосередньо:
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;}
};
То який вихід із цієї парадоксальної ситуації?
(Скажімо також, що legacy_duplicate
це не є безпечним для потоків, але я знаю, що він залишає об'єкт у вихідному стані при його виході. Будучи C-функцією, поведінка лише задокументована, але не має поняття constness.)
Я можу придумати багато можливих сценаріїв:
(1) Однією з можливостей є те, що взагалі немає можливості реалізувати конструктор копій зі звичайною семантикою. (Так, я можу перемістити об’єкт, і це не те, що мені потрібно.)
(2) З іншого боку, копіювання об'єкта за своєю суттю не є безпечним для потоків у тому сенсі, що копіювання простого типу може знайти джерело у напівзміненому стані, тож я можу просто піти вперед і зробити це, можливо,
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) або навіть просто оголошувати duplicate
const і брехати про безпеку ниток у всіх контекстах. (Зрештою, про застарілу функцію це не хвилює, const
тому компілятор навіть не скаржиться.)
class A{
L* impl_;
A(A const& other) : L{other.duplicate()}{}
L* duplicate() const{L* ret; legacy_duplicate(impl_, &ret); return ret;}
};
(4) Нарешті, я можу слідувати логіці та зробити конструктор копій, який бере аргумент 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;}
};
Виявляється, це працює у багатьох контекстах, оскільки зазвичай це об'єкти const
.
Питання в тому, чи це дійсний чи звичайний маршрут?
Я не можу їх назвати, але інтуїтивно очікую, що у мене буде багато проблем із створенням неконструкторного конструктора копій. Ймовірно, він не буде кваліфікуватися як ціннісний тип через цю тонкощі.
(5) Нарешті, хоча це здається надмірним і може мати круті витрати на виконання, я можу додати мютекс:
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;
};
Але змусити це робити виглядає як песимізація і робить клас більшим. Я не впевнений. На даний момент я схиляюся до (4) або (5) або до комбінації обох.
EDIT 1:
Ще один варіант:
(6) Забудьте про все непочуття функції дублюючого члена та просто зателефонуйте legacy_duplicate
з конструктора та заявіть, що конструктор копіювання не є безпечним для потоку. (І за необхідності зробіть інший безпечний потік версії типу, A_mt
)
class A{
L* impl_;
A(A const& other){legacy_duplicate(other.impl_, &impl_);}
};
EDIT 2:
Це може бути хорошою моделлю для того, що виконує спадкова функція. Зауважте, що натискаючи на вхід, виклик не є безпечним для потоку щодо значення, представленого першим аргументом.
void legacy_duplicate(L* in, L** out){
*out = new L{};
char tmp = in[0];
in[0] = tmp;
std::memcpy(*out, in, sizeof *in); return;
}
EDIT 3:
Нещодавно я дізнався, що у мене std::auto_ptr
була схожа проблема з конструктором "копіювати", який не має const. Ефект полягав у тому, що auto_ptr
його не можна було використовувати у контейнері. https://www.quantstart.com/articles/STL-Containers-and-Auto_ptrs-Why-They-Dont-Mix/
legacy_duplicate
неможливо викликати одним і тим же першим аргументом з двох різних потоків.
const
насправді означає. :-) Я б не замислювався над тим, щоб взяти const&
у себе копію копію, доки я не зміню other
. Я завжди думаю про безпеку ниток як про щось, що додається до того, що потрібно отримати з декількох потоків, за допомогою інкапсуляції, і я дуже чекаю на відповіді.
L
якому, змінюється шляхом створення новогоL
примірника? Якщо ні, то чому ви вважаєте, що ця операція не є безпечною для потоків?