#c #undefined-behavior #c 17
#c #неопределенное поведение #c 17
Вопрос:
Рассмотрим следующую, упрощенную и неполную, реализацию вектора фиксированного размера:
template<typename T>
class Vec {
T *start, *end;
public:
Tamp; operator[](ssize_t idx) { return start[idx]; }
void pop() {
end--;
end->~T();
}
template<typename... U>
void push(U... args) {
new (end) T { std::forward<U>(args)... };
end ;
}
};
Теперь рассмотрим следующий T:
struct T {
const int i;
};
И следующий вариант использования:
Vec<T> v;
v.push(1);
std::cout << v[0].i;
v.pop();
v.push(2);
std::cout << v[0].i;
Оператор index использует start
указатель для доступа к объекту. Объект в этот момент был уничтожен pop
, а другой объект был создан в его месте хранения push(2)
. Если я правильно прочитал документацию, связанную с std::launder, это означает, что поведение v[0]
в строке ниже не определено.
Как предполагается использовать std::launder для исправления этого кода? Должны ли мы запускать и завершать launder каждый раз, когда используется placement new? Текущие реализации stdlib, похоже, используют код, аналогичный опубликованному выше. Является ли поведение этих реализаций неопределенным?
Комментарии:
1.
std::vector
не был реализован в стандартном C начиная с C 03.2. @T.C.: Хотите подробнее остановиться на этом? Учитывая, что это, как вы знаете, есть .
3. Возвращаемое значение @LightnessRacesinOrbit
data()
(или в C 03,amp;v[0]
) требуется для разрешения арифметики указателей на него, а арифметика указателей определена только для указателей на один и тот же массив.vector
По очевидным причинам фактически не может использовать массив внутри.4. @T.C.: (Пытаюсь подтвердить мое понимание того, что вы говорите) Предполагая, что вы не считаете динамически выделяемый блок «array» «массивом», вы также считаете, что мы не можем легально выполнять арифметику указателей, скажем, на
new int[N]
?5.В @LightnessRacesinOrbit
vector
нетnew
T[N]
. Он выделяет блок хранилища и один за другим создает элементы в этом хранилище.
Ответ №1:
Как std::launder
предполагается использовать для исправления этого кода? Должны ли мы запускать и завершать launder каждый раз, когда используется placement new?
Начиная с P0532R0, вы могли бы избежать необходимости вызывать, launder()
если возвращаемое значение placement new присваивается end
. Вам не нужно было бы изменять свой начальный указатель, если бы вектор не был пустым, поскольку объект, на который в данный момент указывает start
, все равно имел бы активное время жизни с предоставленным вами кодом.
В том же документе указано, что launder()
это не операция, если срок службы объекта не истек и не был заменен новым объектом, поэтому использование launder()
не приведет к снижению производительности, если в этом нет необходимости:
[…] тип
std::launder(this)
эквивалентен именно этому, как указал Ричард Смит: Помните, чтоlaunder(p)
это не-операция, если p не указывает на объект, время жизни которого закончилось и где в том же хранилище был создан новый объект.
Текущие реализации stdlib, похоже, используют код, аналогичный опубликованному выше. Является ли поведение этих реализаций неопределенным?
Да. P0532R0 также обсуждает эту проблему, и содержание аналогично обсуждению в комментариях к вопросу: vector
не использует размещение new напрямую, возвращаемое значение вызова размещения new теряется в цепочке вызовов функций распределителя вектора, и в любом случае размещение new используется поэлементно, поэтому построение внутреннего векторного механизма в любом случае не может использовать возвращаемое значение. launder()
похоже, это инструмент, предназначенный для использования здесь. Однако тип указателя, указанный распределителем, вообще не обязательно должен быть типом необработанного указателя и launder()
работает только для необработанных указателей. Текущая реализация в настоящее время не определена для некоторых типов; launder()
похоже, что это неподходящий механизм для решения общего случая для контейнеров на основе распределителя.