Страницы

Поиск по вопросам

четверг, 19 декабря 2019 г.

Взаимодействие конструктора (или оператора) перемещения с константным объектом

#cpp #cpp11


Кто хорошо знает Стандарт С++, прошу разъяснить ситуацию.

Среди правил хорошего тона (в том числе, описанных у С. Майерса в его "55 советах...")
есть такое: арифметический оператор должен возвращать константный объект:

const TObject operator + (const TObject &a, const TObject &b);


Это делается, например, для того, чтобы нельзя было написать (a+b)=c или ещё как-то
испортить полученный результат по дороге к его месту назначения.

Теперь, когда появились конструкторы и операторы перемещения, благодаря ним можно
избежать лишнего копирования при таких действиях как

TObject c(a+b);
c = a+b;


Но будет ли вызван конструктор и оператор перемещения в этом случае? Ведь оператор
+ возвращает константный объект, его нельзя изменить (в том числе переместить)! Потому
что перемещение предполагает, что я что-то меняю внутри объекта (например, обнуляю
указатели на выделенную память, чтобы деструктор временного экземпляра не удалил память,
отданную экземпляру c). Часто оператор перемещения реализуется вызовом swap, который
также меняет оба объекта - левый и правый... но правый объект - константная ссылка.

Я заблуждаюсь или нет? Если нет, то как быть с описанным правилом хорошего тона,
когда возвращать нужно константный объект?
    


Ответы

Ответ 1



Не будет вызван конструктор перемещения, но будет вызван конструктор копирования. Не будет, потому что перемещающий конструктор обычно выглядит как T&& и никакого другого конструктора по умолчанию не будет. Конечно, можно определить свой перемещающий конструктор вида const T&&, но что с ним делать? Он бесполезен, т.к. отличаться от копирующего конструктора он не будет ничем. P.S. Это «правило» было сомнительно раньше, а стало ещё и вредным сейчас. Кстати, в C++11 появилось и то, что позволяет явно выразить своё нежелание применения определённых функций к определённому типу аргументов. Так, к примеру, Ваш пример на присваивание можно запретить очень просто: либо определить соответствующий operator= только для lvalue: TObject& operator=(const TObject& rhs) & { //... return *this; } Либо же запретить этот оператор для rvalue явно(предпочтительно): TObject& operator=(const TObject& rhs) && = delete; И всё, никаких правил не нужно выдумывать — Вы контролируете метод использования Вашего класса полностью.

Ответ 2



Здесь имеет место компромисс между эффективностью, с одной стороны, и простотой реализации и интуитивно понятной логикой, с другой стороны. Естественно, когда мы получаем временный объект после выполнения оператора operator +, то достаточно логично выглядит, что вы не можете изменять это временное выражение, как в вашем примере ( a + b ) = c; потому что это нарушает наше представление о подобных операциях вида ( 5 + 2 ) = 10; С другой стороны если возвращается константный объект, то иметь с ним дело через константную rvalue ссылку, большого смысла не имеет, так как вы не сможете изменить состояние объекта, а следовательно все плюсы от возвращения rvalue ссылки теряются. Поэтому если класс определен так, что объекты этого класса содержат много ресурсов, которые нужно освобождать при удалении объекта этого класса или, напротив, нужно создавать при создании объекта этого класса, то желательно определить оператор operator + так, чтобы он возвращал не константный временный объект, и можно было бы к этому объекту эффективно применять перемещающий конструктор или перемещающий оператор присваивания. Именно таким образом объявлен operator + для стандартного класса std::basic_string, то есть он не возвращает константный временный объект из оператора. Однако если вы хотите иметь дело с rvalue ссылками, то это делает код более запутанным. Обратите внимание, что речь может идти не только о возвращаемом типе оператора, но также и о типах параметров этого оператора. Поэтому, имея дело с rvalueссылками, вам придется иметь несколько перегрузок оператора. Например, TObject operator + ( const TObject &a, const TObject &b ); TObject operator + ( const TObject &a, TObject &&b ); TObject operator + ( TObject &&a, const TObject &b ); TObject operator + ( TObject &&a, TObject &&b ); Именно столько перегрузок этого оператора имеется для класса std::basiс_string, когда оба операнда относятся к объектам этого класса. Когда же вы не имеете дело с rvalue ссылками, то вам достаточно объявить лишь одну перегрузку оператора const TObject operator + ( const TObject &a, const TObject &b ); Поэтому для простых классов, объекты которых не являются ресурсо-затратными, этот оператор так и следует определять. В противном случае вам придется делать несколько перегрузок этого оператора. Этот вопрос на самом деле достаточно серьезный, так как в настоящий момент в стандарте C++ имеется дефект относительно таких стандартных функций как std::min, std::max и std::minmax, которые работают только с константными ссылками, когда эти функции имеют дело с двумя аргументами. Я уже обратил внимание комитета по стандартизации C++, что эти функции следует переписать так, чтобы они могли иметь дело с не константными ссылками и, в частности, с rvalue ссылками в слзданной мною теме Overloading std::min, std::max, and std::minmax for non-constant references.

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

Отправить комментарий