У меня есть обертка для какого-то унаследованного кода.
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-функцией, поведение только документируется, но не имеет понятия постоянства.)
Я могу думать о многих возможных сценариях:
(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) Наконец, я могу следовать логике и создать конструктор копирования, который принимает неконстантный аргумент.
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) или их комбинации.
РЕДАКТИРОВАТЬ 1:
Другой вариант:
(6) Забудьте обо всех бессмысленных повторяющихся функциях-членах и просто вызовите legacy_duplicate
конструктор и объявите, что конструктор копирования не является потокобезопасным. (И, если необходимо, создайте другую поточно-ориентированную версию этого типа A_mt
)
class A{
L* impl_;
A(A const& other){legacy_duplicate(other.impl_, &impl_);}
};
РЕДАКТИРОВАТЬ 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;
}
РЕДАКТИРОВАТЬ 3:
Я недавно узнал, что std::auto_ptr
была похожая проблема с неконстантным конструктором копирования. Эффект был в том, что auto_ptr
нельзя было использовать внутри контейнера. https://www.quantstart.com/articles/STL-Containers-and-Auto_ptrs-Why-They-Dont-Mix/
legacy_duplicate
не может быть вызвана с одним и тем же первым аргументом из двух разных потоков.
const
самом деле значит. :-) Я бы не подумал дважды о том, чтобы взять const&
в свой экземпляр ctor, если я не изменяю other
. Я всегда думаю о безопасности потоков как о чем-то, что добавляется к тому, что необходимо получить из нескольких потоков, через инкапсуляцию, и я действительно жду ответов.
L
которое изменяется путем создания новогоL
экземпляра? Если нет, то почему вы считаете, что эта операция не является поточно-ориентированной?