Изменение объекта const через указатель, полученный во время построения
Я только что обнаружил, насколько легко изменять объекты const без какой-либо черной магии const_cast
. Рассмотрим:
#include <iostream>
class Test {
public:
Test(int v)
:m_val{ v },
m_ptr{ &m_val }
{}
int get() const { return m_val; }
void set(int v) const { *m_ptr = v; }
private:
int m_val;
int* m_ptr;
};
int main()
{
const Test t{ 10 };
std::cout << t.get() << '\n';
t.set(0);
std::cout << t.get() << '\n';
return 0;
}
В последних версиях Clang, GCC и MSVC не отображаются предупреждения и производятся ожидаемые результаты:
10 0
Является ли это четко определенным поведением в соответствии с текущим стандартом? Если он undefined что, если m_val
имел тип std::aligned_storage_t<sizeof(int), alignof(int)>
и конструктор new
'ed int
в нем? Я считаю, что это довольно распространенный случай, когда речь идет о оптимизации небольших буферов.
Edit
Спасибо, кажется, это просто еще один способ застрелить себя ногой.
Что беспокоит, кажется, что это:
struct Test2 {
int i;
void operator()() { ++i; }
};
const std::function<void()> f{ Test2{ 10 } };
f();
также является undefined, когда реализация решает сохранить объект Test2
внутри f
(и что случай в libС++ и в Visual Studio)
Ответы
Ответ 1
const
применяет "поразрядную константу", но то, что вы обычно хотите, это "логическая константа".
В случае объекта, содержащего указатель, это означает, что функция-член const не может изменять сам указатель, но может изменять то, на что ссылается указатель.
Это хорошо известно давно.
Чтобы получить логическую константу, вы 1) используйте mutable
(или иногда const_cast
), чтобы разрешить модификацию членов, которые не влияют на логическое состояние объекта (например, кешированные значения /memoization ) и 2) обычно имеют для ручного принуждения не записывать данные через указатель (но если это владеющий указатель, это право собственности, вероятно, должно быть делегировано объекту, который управляет только владением этими данными, и в этом случае создание его const обычно должно препятствовать записи данных, которыми он владеет).
Что касается конкретной детали наличия указателя non-const, указывающего на данные, которые могли бы быть изменены, то вы в основном получаете (постоянную) версию примерно того же, что const_cast
как правило, используется: получить неконстантный доступ к данным, к которым вы в противном случае имели бы указатель const
. Это зависит от вас, чтобы вы использовали это только таким образом, чтобы это не вызывало проблемы (но просто наличие и/или запись через этот указатель сами по себе не обязательно приводят к проблеме).
Другими словами, мы имеем здесь два отдельных указателя на некоторые данные. this
позволяет вам получить доступ к данным объекта. В функции члена const
вы можете читать (не) записывать данные через this
, если только (как отмечено выше) не было отмечено mutable
. В этом случае вы сохраняете второй указатель на одни и те же данные. Поскольку ничего не стоит отмечать как указатель на const
, это не так, поэтому вы получаете неконстантный доступ к данным, на которые он указывает.
Ответ 2
Как указывали другие комментарии: вы изменяете объект, на который указывает m_ptr
. Этот объект "указал" не является частью class Test
(насколько это видит компилятор). Вот почему компилятор позволяет это сделать.
Сказав это, я считаю, что это будет поведение undefined. Это потому, что m_ptr
фактически указывает на другую переменную-член (m_val
) объекта const Test t
! Составителям разрешено оптимизировать аргументы, и они могут полагаться на константу, чтобы сделать это.
Единственное исключение - вы используете ключевое слово mutable
, но это другая история.
Ответ 3
В принципе в С++ существуют два типа константы: физическая константа и логическая константа.
Что касается физической консистенции, то все это вполне справедливо в рассматриваемой части кода, потому что set()
изменяет значение, на которое указывает m_ptr
, а не сам указатель, который является частью класса.
Здесь нарушается логическая константа. Но есть много способов в С++ нарушить логическую константу, потому что этот тип константы много зависит от конкретного дизайна класса.
В приведенном выше примере программа ведет к UB, потому что она пытается изменить объект const.
Из n4296, 7.1.6.1 cv-квалификаторы:
За исключением того, что любой член класса, объявленный mutable (7.1.1), может быть изменен, любая попытка изменить объект const в течение его жизненного цикла (3.8) в режиме undefined.
Ответ 4
Это поведение undefined. Не все объявленные типы const
действительно являются константами, поэтому не всегда поведение undefined должно изменять что-то, объявленное таким образом. У вас может быть ссылка на тип const, который ссылается на неконстантное неконстантное значение, отбрасывает константу и изменяет значение без вызова поведения undefined. В этом случае, хотя исходное определение const
, поэтому вы должны считать его константой.
Модификация любой константы - это поведение undefined и да, существует множество способов "случайно" сделать это. В версии aligned_storage, да, это поведение undefined для изменения этих постоянных данных с помощью нового места размещения для изменения.