Pull to refresh

Comments 27

С одной стороны, задавать подобные вопросы на собеседовании — дело бесполезное, это почти ничего не говорит о способностях интервьюируемого.
Все бы так собеседования проводили :)
Определение mutable в классе зачастую излишне раскрывает внутреннюю жизнь класса. Если вы делаете класс для внешнего API, то изменения его внутренней структуры, той же технологии кеширования, как в статье, заставляет пользователей вашего API перекомпилировать свои программы.
Поэтому мне больше по вкусу скрытая реализация внутренних данных, как это использовано в Qt.
class ObjectPrivate;

class QObject {
private
    QObjectPrivate *d;
public:
// ... any const methods
};
И тёмные стороны PIMPL имеет. Для стабилизации ABI подходит как нельзя лучше, но лишним разыменованием и использованием кучи, омрачает славный перформанс C++. Что в раз очередной сподвигает нас на предварительный анализ задачи и выбор правильных инструментов для её решения.
Использовать кучу правило или не использовать выбираете вы. Примеров есть кучи, где d на структуру, в контейнере выделенную один раз, указывает.

Не, пусть Йоде будет йоднутое. Короче, вас никто не обязывает выделять на каждый объект отдельную структуру из кучи. В стеке, конечно, выделить так память не получится, но можно заранее отложить память в объекте-контейнере/фабрике под N объектов.

Что да, в очередной…
Только если для внешнего API.
В остальных случаях это только утруднит написание и ухудшит производительность кода.
Оба подхода нужны. Я просто считаю что такие решения должны приниматься из практических соображений, а не быть делом вкуса.
Поэтому мне больше по вкусу скрытая реализация внутренних данных, как это использовано в Qt.
Возможность изменения реализации без перекомпиляции — это же совсем другая проблема, здесь не рассматриваемая.

А решение проблемы спецификаторов const в публичном API на QObject, придётся так или иначе передать классу QObjectPrivate, и там снова думать — использовать mutable, или что-то другое.
Вы хорошо подумали?
QObjectPrivate, во-первых, не публичный, его структура скрыта, во-вторых, не обязан иметь const методов.
Я почему-то подумал, что внутри const-метода нельзя вызывать не-const методы по указателю, который описан в классе:
// QObject.h

class QObject {
        class QObjectPrivate;
        QObjectPrivate *d;
public:
        int area() const;
};

// QObject.cpp

class QObject::QObjectPrivate
{
public:
        int area() { return 0; }
};

int QObject::area() const
{
        return d->area(); // вот здесь вызов не-const метода
}

Но этот пример у меня скомпилировался.

Другое дело, если бы QObjectPrivate был не указателем, а членом класса QObject.
// QObject.h

class QObjectPrivate
{
public:
        int area() { return 1; }
};

class QObject {
        QObjectPrivate d;
public:
        int area() const;
};

// QObject.cpp

int QObject::area() const
{
        return d.area(); // вот здесь ошибка
}
Я думал, что поведение должно быть аналогичным.
Да, в вызове d->area() значение указателя, лежащее в QObject, не меняется, следовательно для компилятора константность QObject соблюдена.
Еще обратите внимание, что во втором случае, когда QObjectPrivate является членом класса QObject, вам пришлось перенести его определение в .h, т.е. раскрыть детали его реализации пользователям QObject. Что не всегда желательно.
Не знаю, кем mutable забыт, но мне иногда приходится его использовать.
для многопоточных данных\флагов — и сам делал, но вот как часто кто-то добавляет мутабельности константным методам класса?
Вы случайно с volatile не путаете?

В стаье незаслуженно забыт старый добрый const_cast. Он кстати позволяет скрыть текущие абстракции (в данном случае текущие конвенции) и перенести их из заголовков в реализацию, где им и место.

А не будет ли это UB?
Если я сниму const с класса через const_cast, и вызову его не-const метод.
Только если у исходного объекта, у которого вызывается метод стоит квалификатор const. Если объект не константный, то его можно будет изменить через const_cast даже внутри const метода.
Интересно. Вот в таком примере:
int GLOBAL;

struct point 
{ 
        int x;

        int calc() const {
                GLOBAL = x*17;
                change();
                return x*17;
        }

        void change() const {
                point* self = const_cast<point*>(this);
                self->x++;
        }
};

компилятор не может полагаться на то, что значение 'x' у const-объекта *this не будет изменено при вызове ф-ции change()?

В общем-то, играясь с разными опциями компилятора мне не удалось заставить его выполнить common sub-expression elimination, т.е. умножение на 17 он всегда честно делает 2 раза, независимо от опций оптимизации.
Есть мнение, что const — довольно бесполезная, а местами даже вредная штука.

Вредность в том что снимая const с одной функции, вы вынуждены бежать по всему коду и снимать const'ы со всего кода который эту функцию вызывает, и так рекурсивно.

И наоборот, захотев добавить const на функцию, вы вынуждены обвешивать const'ами все что она вызывает. Причем в процессе вам может понадобиться потрогать код сторонней библиотеки — страдайте и обмазывайтесь const_cast'ами и mutable'ами.

Теперь про бесполезность. const не дает никаких гарантий вызывающему коду, поэтому непонятно зачем он в сигнатуре.

А не дает никаких гарантий потому что const имеет право изменять:

1. Аргументы. Среди которых (возможно через множество промежуточных звеньев) может оказаться this, только уже без const'а

2. Статические переменные. Среди которых опять же может оказаться this.

3. mutable, рассмотренный в этой статье.

4. И вообще, можно же взять и memset зафигачить. Что-то где-то поменяется.

5. Самое печальное, что были прецеденты когда это непредумышленно происходило. И ложная надежда «const значит this не поменяется» только мешала.
const нужен в качестве:
1. дополнительной проверки корректности программ компилятором. Да, грязные хаки могут менять состояние константного объекта, но в корректном коде они не нужны и практически отсутствуют (за исключением mutable примитивов синхронизации). А вот очевидные ошибки/опечатки будут своевременно отловлены.
2. дополнительной информации об API для вызывающей стороны.
3. перегрузка по const/non-const позволяет проводить некоторые оптимизации.
Причем в процессе вам может понадобиться потрогать код сторонней библиотеки — страдайте.

для этого и существуют mutable/const_cast. Зловредную библиотеку можно вызывать через const-корректный адаптер
1. Я вам целый список предоставил, способов которыми объект может измениться в результате вызова const-метода. Из них грязным хаком я бы назвал const_cast. Но вообще-то все эти механизмы входят в стандарт и UB, например, не вызывают.

2. Какой дополнительной информации? Вы точно прочитали текст на который отвечаете?

3. О, спасибо что напомнили. Запишите это тоже в минус: необходимость дублирования кода для const/не-const бесит, приводит к ошибкам, часто содержит в себе const_cast или является темплейтом (что привносит свои, темплейтные проблемы)

4. Она не зловредная. Она просто не расставила const везде где это возможно (и никто этого не делает, вы в том числе). Хотя бы просто потому что у нее не было инструмента который бы это сделал.
1. использование любого из этих методов не по прямому назначению подходит под правила дурного тона и является легитимным основанием отклонить ваш код на ревью. memcpy в с++ практически никогда не нужен.
2. например, если в библиотеке есть функция, принимающая const T &val, она (с поправкой на профпригодность автора) не будет менять val.
3. к каким ошибкам?
4. всегда пишу const-корректный код. За последний год использовал const_cast единожды (и то в вызове сишной библиотеки). Мозг, глаза и руки — достаточный набор инструментов для написания корректных программ.

Я уже не говорю про то, что иногда const-корректность необходима для достижения желаемого поведения

п.с. есть еще noexcept, который, между прочим, может оптимизировать выполнение некоторых операций. Его вы тоже не рекомендуете к использованию только потому, что лень печатать?
Статические переменные. Среди которых опять же может оказаться this.

Можно пример того, как this оказывается статической переменной?
Указатель со значением, равным this, вполне может лежать в статической переменной. Думаю, именно это и имелось в виду.
Понятно. Напоминает старый анекдот:
Обитатель одной квартиры подал жалобу, что в его окно можно увидеть голых женщин в бане через дорогу. Приходит инспектор:
— Да что Вы такое говорите, ничего не видно!
— А возьмите бинокль и залезьте на шкаф!
Впечатления от таких вопросов и ответов у меня двоякие. С одной стороны, задавать подобные вопросы на собеседовании — дело бесполезное, это почти ничего не говорит о способностях интервьюируемого

При написании const-корректных и многопоточных программ, mutable рано или поздно (скорее рано) понадобится. На мой взгляд, претендент на позицию с++ миддла не обязан знать все тонкости с++11/14/17, но базовые вещи из с++03 знать обязан
Имхо, mutable нужно редко, но в тех законных случаях, когда да — без него не обойтись.
Я бы отнес mutable (как и const_cast, memset, raw new, и многое другое) к разряду «грязных хаков». Да, ими вполне можно и даже нужно пользоваться, но относительно редко, и, как правило, лишь для цели достижения максимальной производительности и лишь в некоторых служебных местах. Поэтому любое их использование должно быть надежно инкапсулировано.
Sign up to leave a comment.