понедельник, 5 февраля 2024 г.

[prog.c++] Захотелось в C++ странного (на тему транзитивной константности)...

Недавно столкнулся с задачей, в которой было бы хорошо иметь транзитивную константность в C++.

Транзитивная константность -- штука своеобразная. И, по большей части, мне нравится, что в C++ ее нет. Но вот оказался в ситуации, когда она была бы в тему.

Что такое транзитивная константность?

Представим себе, что у нас есть:

class Foo {
public:
  void foo(); // Это не-const метод.
  void foo2() const// А это уже const-метод.
};

class Bar {
  Foo * m_foo; // Это не-const указатель!
public:
  ...
  void bar2() const { // Это const-метод, в котором мы не можем присвоить m_foo новое значение.
    m_foo->foo(); // Но зато можем вызвать не-const метод для m_foo.
  }
};

Foo foo;
const Bar bar{&foo};
bar.bar2(); // Этот вызов может изменить foo.

Из-за того, что в C++ константность не транзитивна, то в const-объекте bar можно иметь не-const указатель на foo и в const-методе Bar::bar2 можно поменять объект foo.

Если бы константность была транзитивной, то в Bar::bar2 указатель Bar::m_foo автоматически бы стал константным и вызвать в Bar::bar2 не-const метод Foo::foo у нас уже не получилось бы.

Поскольку в С++ транзитивной константности нет, то я было попробовал сделать ее вручную. По типу чего-то такого:

template<typename T>
class AutoConstPtr {
  T * m_ptr;
public:
  ...
  [[nodiscard]] T * get() { return m_ptr; } // Не-const.
  [[nodiscard]] const T * get() const { return m_ptr; } // Уже const.
};

Это позволяет получить транзитивную константность в простом случае:

class Bar {
  AutoConstPtr<Foo> m_foo; // Это уже не raw pointer.
public:
  ...
  void bar2() const {
    m_foo.get()->foo(); // А вот здесь будет ошибка компиляции!
  }
};

И это уже было именно то, что мне нужно. И, казалось бы, счастье было уже так близко...

Но, к сожалению, это не сработало на практике. Например, из-за вот таких случаев:

void ProcessItems(const std::vector<AutoConstPtr<Foo>> & items) {
  for(auto p : items) {
    p.get()->foo(); // Упс!
  }
}

Фокус в том, что p -- это будет копия AutoConstPtr<Foo>. Не-const копия. Следовательно, для p будет вызываться не-const версия get. Следовательно, будет возвращаться не-const указатель на Foo. Следовательно, можно вызывать не-const методы Foo, т.е. модифицировать Foo. И это в ситуации, когда исходно у нас были как раз константные указатели на Foo (ведь у нас const-ссылка на вектор указателей).

Вот таким вот незамысловатым образом красивая идея накрылась медным тазом. Абыдна, да 🙁

И вот тут мне захотелось, чтобы в C++ при описании конструктора можно было бы явно описать, применяется ли этот конструктор для const-объекта или нет.

Ведь сейчас мы пишем что-то вроде:

MyClass::MyClass(const MyClass & other) {...}

и понимаем, что это конструктор копирования. Но не понимаем, какой именно экземпляр MyClass при этом конструируется. Т.е.:

MyClass source{...};

MyClass copy1{source}; // Вызов конструктора копирования.
const MyClass copy2{source}; // Вызов того же самого конструктора копирования.

А что, если бы мы могли добавлять const и к конструктору?

template<typename T>
class AutoConstPtr {
  T * m_ptr;
public:
  ... // Здесь какие-то другие конструкторы.
  AutoConstPtr(const AutoConstPtr &) = delete// Нельзя построить не-const из const.
  AutoConstPtr(const AutoConstPtr & other) const // Тут все OK.
    : m_ptr{other.m_ptr}
  {}
  ...
};

Тогда бы не получилось бы скомпилировать конструкцию:

for(auto p : items) ...

потому что нельзя построить не-const объект AutoConstPtr из const-объекта.

А вот так бы получилось бы:

for(const auto p : items) ...

Вот такая вот странная фантазия.

Но это реально фантазия, т.к. даже если бы была возможность помечать конструкторы как const, то непонятно было бы что делать вот с такими ситуациями:

const auto std::vector<AutoConstPtr<Foo>> & source = ...;
std::vector<AutoConstPtr<Foo>> selected;
std::copy_if(source.begin(), source.end(),
    std::back_inserter(selected),
    [](const auto & item) { return ...; });

Так что, возможно, идея транзитивной константности в принципе не для C++.

Комментариев нет: