Изменение объекта 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 для изменения этих постоянных данных с помощью нового места размещения для изменения.