почему встроенное присваивание возвращает неконстантную ссылку вместо константной ссылки в С++?

(обратите внимание, что в заголовке исходного вопроса было «вместо rvalue», а не «вместо ссылки на константу». Один из приведенных ниже ответов является ответом на старый заголовок. Это было исправлено для ясности)

Одна общая конструкция в C и C++ предназначена для цепных назначений, например.

    int j, k;
    j = k = 1;

Второй = выполняется первым, при этом выражение k=1 имеет побочный эффект, заключающийся в том, что k устанавливается в 1, в то время как значение самого выражения равно 1.

Однако в C++ (но не в C) разрешена следующая конструкция, которая допустима для всех базовых типов:

    int j, k=2;
    (j=k) = 1;

Здесь выражение j=k имеет побочный эффект установки j на 2, а само выражение становится ссылкой на j, которая затем устанавливает j на 1. Насколько я понимаю, это потому, что выражение j=k возвращает non-const int&, например вообще говоря, lvalue.

Это соглашение обычно также рекомендуется для определяемых пользователем типов, как описано в «Пункт 10. Операторы присваивания должны возвращать (неконстантную) ссылку на *this» в Эффективный C++ Мейерса(дополнение в скобках мое). Этот раздел книги не пытается объяснить, почему ссылка не является const, или даже отметить неconstность мимоходом.

Конечно, это, безусловно, добавляет функциональности, но утверждение (j=k) = 1; выглядит, мягко говоря, неуклюжим.

Если бы соглашение заключалось в том, чтобы встроенное присваивание возвращало ссылки const, то пользовательские классы также использовали бы это соглашение, и исходная конструкция цепочки, разрешенная в C, по-прежнему работала бы без каких-либо посторонних копий или перемещений. Например, следующее выполняется правильно:

#include <iostream>
using std::cout;

struct X{
  int k;
  X(int k): k(k){}
  const X& operator=(const X& x){
  // the first const goes against convention
    k = x.k;
    return *this;
  }
};

int main(){
  X x(1), y(2), z(3);
  x = y = z;
  cout << x.k << '\n'; // prints 3
}

с преимуществом в том, что все 3 (встроенные C, встроенные C++ и пользовательские типы C++) не допускают использования идиом, таких как (j=k) = 1.

Было ли преднамеренным добавление этой идиомы между C и C++? И если да, то какая ситуация оправдывает его использование? Другими словами, какую неложную выгоду дает это расширение функциональности?


person xdavidliu    schedule 10.08.2017    source источник
comment
Я думаю, что (j=k) = 1; является следствием обобщения правил, а не преднамеренной особенностью. Я не мог себе представить, чтобы подобный код использовался каким-либо практическим образом. Может быть некоторое преимущество в том, что operator= возвращает lvalue, поскольку вы можете передать результат присваивания по ссылке в функцию, но у меня нет конкретных примеров этого навскидку.   -  person Silvio Mayolo    schedule 10.08.2017
comment
Согласны ли вы с ответом a, потому что так сказано в стандарте, или вы ищете, почему так сказано в стандарте?   -  person NathanOliver    schedule 10.08.2017
comment
Натан Оливер, я не могу понять, о чем вы спрашиваете; можешь перефразировать?   -  person xdavidliu    schedule 10.08.2017
comment
@xdavidliu Вы согласны с ответом, в котором говорится, что причина, по которой это разрешено, заключается в том, что ‹какой-то раздел стандарта C++› говорит, что это так, или вы ищете ответ, в котором говорится, почему в этом разделе стандарта говорится, что он должен вернуться ссылка lvalue.   -  person NathanOliver    schedule 10.08.2017
comment
ах ладно, в этом случае я определенно хотел бы знать, почему стандарт говорит так. Поскольку эта функция присутствует для C++, но не для C, я предполагаю, что это сознательное решение, поэтому должна быть какая-то ситуация, когда это оправдано.   -  person xdavidliu    schedule 10.08.2017
comment
Название вашего вопроса вводит в заблуждение. Константность возвращаемой ссылки и то, возвращает ли она lvalue или rvalue, — это разные вопросы.   -  person kraskevich    schedule 10.08.2017
comment
@kraskevich Я смутно осознаю, что есть исключения из наивного определения lvalue и rvalue, но я думал, что эти исключения на самом деле не относятся к встроенным функциям, например. для встроенных функций, таких как int и double, lvalue полностью синоним неконстантной ссылки, например. что-то, что вы можете поместить в левой части задания?   -  person xdavidliu    schedule 10.08.2017
comment
Чем больше и больше я смотрю на это, кажется, что причина, по которой они различаются, заключается в том, что у C нет другого выбора, кроме как вернуть rvalue. В C нет ссылок, поэтому C не может вернуть ссылку lvalue, как C++. И возврат ссылки, как правило, является нейтральным/приростом производительности.   -  person NathanOliver    schedule 10.08.2017
comment
@xdavidliu Левая часть задания - плохая аналогия. Я предпочитаю следующее упрощение: если у него есть имя, это lvalue. Если это не так, это rvalue.   -  person kraskevich    schedule 10.08.2017
comment
@kraskevich Первая половина верна, но многое другое (например, предварительное приращение, разыменование (с *, ->, .* или ->*), подписка, несколько результатов вызова функций, операторы-запятые, тернарные выражения и (как в этом вопросе) присваивание) являются lvalues ​​без имен. Всегда правильное правило, которое примерно такое же простое, звучит так: «Что-то является lvalue, если вы можете взять его адрес».   -  person Daniel H    schedule 10.08.2017
comment
Нынешнее название вопроса не имеет особого смысла. Константные ссылки и lvalue не противоположны. Это все равно, что спросить, почему человек покупает мясо вместо яблок.   -  person kraskevich    schedule 10.08.2017
comment
@kraskevich Я не думаю, что тот факт, что эти две вещи не являются полными противоположностями, сильно умаляет достоинства вопроса, но на случай, если другие люди сочтут этот факт явно отвлекающим, я изменю его.   -  person xdavidliu    schedule 10.08.2017
comment
Я только сегодня подумал задать этот вопрос, а ты задал его на прошлой неделе. :-) Я нахожу ответ, который вы выбрали, неудовлетворительным. :-/   -  person Omnifarious    schedule 14.08.2017


Ответы (3)


По замыслу одно фундаментальное различие между C и C++ заключается в том, что C — это язык отбрасывающий lvalue, а C++ — сохраняющий lvalue.

До C++98 Бьерн добавил ссылки на язык, чтобы сделать возможной перегрузку операторов. А ссылки, чтобы быть полезными, требуют, чтобы ценность выражений сохранялась, а не отбрасывалась.

Эта идея сохранения lvalueness не была формализована до C++98. В обсуждениях, предшествовавших стандарту C++98, тот факт, что ссылки требуют сохранения lvalue выражения, был отмечен и формализован, и именно тогда C++ сделал один серьезный и целенаправленный отход от C и стал языком, сохраняющим lvalue.

C++ стремится сохранить "lvalueness" любого результата выражения, пока это возможно. Это относится ко всем встроенным операторам, а также к встроенному оператору присваивания. Конечно, это не делается для того, чтобы можно было записывать такие выражения, как (a = b) = c, поскольку их поведение было бы неопределенным (по крайней мере, в исходном стандарте C++). Но из-за этого свойства C++ вы можете написать такой код, как

int a, b = 42;
int *p = &(a = b);

Насколько это полезно — другой вопрос, но опять же, это лишь одно из следствий сохранения lvalue дизайна выражений C++.

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

person AnT    schedule 10.08.2017
comment
интересный. Кроме того, в какой ситуации (a=b) вернет lvalue, но (a=b) = c не будет вести себя ожидаемым образом? Несколько современных компиляторов, на которых я это пробовал, работают без сучка и задоринки. Разве исходный стандарт С++ не требует оценки (a=b) как чего-то, что можно присвоить? - person xdavidliu; 10.08.2017
comment
@xdavidliu: В соответствии с классической спецификацией C++98 в (a = b) = c нет последовательности. По этим старым правилам это выражение на самом деле не просит компилятор сделать a = b первым и a = c вторым, как может показаться на первый взгляд. На самом деле это всего лишь две непоследовательные попытки изменить a, что делает поведение неопределенным. - person AnT; 10.08.2017
comment
Однако правда в том, что исходная спецификация в порядке в контексте с отбрасыванием lvalue (в C), но не имеет смысла (испорчена) в контексте с сохранением lvalue (в C++). По сути, это инициировало полную переработку модели секвенирования C++ в C++11. А в современном С++ (a = b) = c четко определен. Вот почему вы видите, что компиляторы ведут себя так, как ожидалось. - person AnT; 10.08.2017
comment
Что касается того, почему это не const lvalue... Честно говоря, я не понимаю, почему это должно быть. если я не ошибаюсь, ссылка const (это то, что было предложено в вопросе) все равно будет rvalue, а не lvalue - person xdavidliu; 10.08.2017
comment
@xdavidliu: это неверно. Классическая ссылка всегда является lvalue, независимо от того, является ли она const или нет. - person AnT; 10.08.2017
comment
Я могу что-то неправильно понять, но, исходя из определения lvalue как чего-то, адрес которого мы можем взять, если мы попробуем X *p = &(y = z); в примере в конце вопроса, код не скомпилируется, потому что он пытается чтобы взять адрес возвращаемой константной ссылки, и, следовательно, (y = z) не является lvalue - person xdavidliu; 10.08.2017
comment
@xdavidliu: Да, lvalue — это то, что мы можем взять по адресу (не на 100% точное, но достаточно хорошее). Ваш код с X не компилируется просто потому, что нарушает базовую константную правильность. Вам нужно const X *p = &(y = z);. Это отлично скомпилируется. Ошибка не имеет ничего общего с тем, что ссылка const якобы не является lvalue. (y = z) является lvalue. В вашем случае это просто const-qualified. - person AnT; 11.08.2017
comment
Почему было принято решение создать язык, сохраняющий C++ и lvalue, если не разрешить создание очень запутанных выражений, включающих присваивание? И когда датируется это решение? Я использую C++ со времен cfront и никогда не слышал об этом. - person Omnifarious; 15.08.2017
comment
Решение было принято потому, что C++ нуждался в полной фундаментальной переработке того, как он обрабатывает lvalue: язык хотел ввести новую концепцию — ссылки (чье введение, в свою очередь, было вызвано перегружаемыми операторами). ). Это позволило C++ обрабатывать, передавать и возвращать lvalue непосредственно (вместо того, чтобы делать это через указатели). Именно это привело к серьезной переработке встроенных операторов в C++, что сделало их полностью отличными от их поверхностных аналогов в C. Решение датируется самым первым стандартом C++ - C++98, т.е. язык C++ был таким с самого начала. - person AnT; 15.08.2017
comment
Хотя можно сказать, что первоначальная спецификация была неполной, поскольку вносила серьезные дефекты в секвенирование (сохранение lvalue несовместимо с секвенированием в стиле C). Только в C++11 эти дефекты были окончательно исправлены. Как так вышло, что вы никогда не слышали об этом - я не знаю. - person AnT; 15.08.2017
comment
@AnT - я тоже не знаю, возможно, потому, что мир не был так связан, и я не был постоянным участником comp.lang.c++, когда работал над стандартом C++98. Но теперь я услышал об этом. Спасибо. Это ответ, который я действительно хотел получить на этот вопрос, и он имеет большой смысл. Я знал, что ссылки были обязательным дополнением, чтобы заставить работать перегрузку операторов. И включение его в этот контекст имеет большой смысл. Еще раз спасибо. У меня возникает соблазн отредактировать ваш комментарий в ваш основной ответ. :-) - person Omnifarious; 24.08.2017
comment
Не согласен с первым абзацем - это не особо принципиальная разница, речь идет только о ценностной категории результата присваивания и условных операторах. Это почти никогда не встречается в реальном кодировании - person M.M; 24.08.2017
comment
@MM - Почему также условно? Это кажется довольно странным. - person Omnifarious; 24.08.2017
comment
@Omnifarious Очень полезно, чтобы условное выражение могло давать lvalue, например. (foo ? a : b) = 5; или func(foo ? a : b), где func принимает аргумент по неконстантной ссылке. На самом деле это не новая функциональность, потому что вы могли бы написать *(foo ? &a : &b) = 5;, но это выглядит аккуратно. - person M.M; 24.08.2017
comment
@M.M: Ну, это также включает в себя оператор запятой и приращение/уменьшение префикса. И хотя поначалу это могло показаться не принципиальным, противоречия между исходным подходом C к секвенированию и операторами C++, сохраняющими lvalue, вызвали глобальную переработку системы секвенирования C++. Что является довольно фундаментальным изменением. - person AnT; 26.08.2017
comment
Я думал, что изменение последовательности С++ 11 было связано с поддержкой потоков (C11 следовал той же модели). - person M.M; 26.08.2017

Отвечу на вопрос в заголовке.

Предположим, что он вернул ссылку rvalue. Таким образом было бы невозможно вернуть ссылку на вновь назначенный объект (потому что это lvalue). Если невозможно вернуть ссылку на вновь назначенный объект, необходимо создать копию. Это было бы ужасно неэффективно для тяжелых предметов, например контейнеров.

Рассмотрим пример класса, похожего на std::vector.

С текущим типом возвращаемого значения назначение работает следующим образом (я намеренно не использую шаблоны и идиому копирования и замены, чтобы сделать код максимально простым):

class vector {
     vector& operator=(const vector& other) {
         // Do some heavy internal copying here.
         // No copy here: I just effectively return this.
         return *this;
     }
};

Предположим, что он вернул rvalue:

class vector {
     vector operator=(const vector& other) {
          // Do some heavy stuff here to update this. 
          // A copy must happen here again.
          return *this;
      }
};

Вы можете подумать о возврате ссылки на rvalue, но это тоже не сработает: вы не можете просто переместить *this (иначе цепочка присвоений a = b = c запустит b), поэтому для ее возврата также потребуется вторая копия.

Вопрос в теле вашего сообщения другой: возврат const vector& действительно возможен без каких-либо осложнений, показанных выше, поэтому для меня это больше похоже на соглашение.

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

person kraskevich    schedule 10.08.2017
comment
хорошо, но заголовок вопроса относится к встроенным, а не к определяемым пользователем объектам? - person xdavidliu; 10.08.2017
comment
@xdavidliu Я считаю, что дело в последовательности. Я бы не хотел, чтобы встроенные и пользовательские типы вели себя по-разному. - person kraskevich; 10.08.2017
comment
с точки зрения согласованности текущее соглашение приводит к тому, что встроенные функции C++ согласуются с пользовательскими типами C++, оба из которых несовместимы с C. Однако, если бы соглашение возвращало константные ссылки, то все три быть последовательным, так как все три не допускают выражений типа (j = k) = 3. - person xdavidliu; 10.08.2017
comment
@ kraskevich Я включил в исходный вопрос пример работы const ref, как вы заметили. Что касается согласованности, почему бы не иметь все 3 согласованных (встроенные функции C == встроенные функции C++ == пользовательские C++) вместо только 2 (встроенные функции C! = встроенные функции C++ == пользовательские функции C++)? - person xdavidliu; 10.08.2017
comment
@xdavidliu Почему это должно соответствовать C? C не является подмножеством C++. Это совершенно другой язык. Они не имеют ничего общего друг с другом. Вы же не ожидаете, что он будет совместим, скажем, с Java, не так ли? - person kraskevich; 10.08.2017
comment
с точки зрения встроенных типов, таких как int, double, массивы, указатели и т. д. большая часть семантики C напрямую переносится на C++, поскольку C++ изначально был построен поверх C, а не поверх Java или любого другого языка. . C++ — это совсем другой язык с кучей новых функций, конечно, но совместимость с C, возможно, была не нулевой проблемой во время разработки C++. Любое изменение идиомы должно иметь вескую причину. Разница здесь только, кажется, допускает непривлекательные идиомы, как те, которые я предоставил, что, похоже, не является убедительной причиной для разницы, отсюда и мой вопрос. - person xdavidliu; 10.08.2017
comment
@xdavidliu, как указал AnT, зачем делать operator = особым случаем таким образом? В любом случае разница по большому счету несущественная. Он позволяет вам писать программы на C++, которые нельзя скомпилировать на C, но в любом случае это довольно легко сделать. - person Omnifarious; 24.08.2017

Встроенные операторы ничего не "возвращают", не говоря уже о "возвращении ссылки".

Выражения характеризуются в основном двумя вещами:

  • их тип
  • их стоимостная категория.

Например, k + 1 имеет тип int и категорию значения "prvalue", а k = 1 имеет тип int и категорию значения "lvalue". lvalue — это выражение, обозначающее ячейку памяти, а ячейка, обозначенная k = 1, — это та же ячейка, которая была выделена объявлением int k;.

Стандарт C имеет только категории значений "lvalue" и "not lvalue". В C k = 1 имеет тип int и категорию "не lvalue".


Кажется, вы предлагаете, чтобы k = 1 имел тип const int и категорию значений lvalue. Возможно, можно было бы, язык был бы немного другим. Это поставило бы вне закона запутанный код, но, возможно, запретил бы и полезный код. Это решение трудно оценить разработчику языка или комитету по разработке, потому что они не могут придумать все возможные способы использования языка.

Они ошибаются в том, что не вводят ограничений, которые могут оказаться проблемой, которую еще никто не предвидел. Связанный пример: Должны ли неявно сгенерированные операторы присваивания быть & ref-квалифицированными? .

Одна из возможных ситуаций, которая приходит на ум:

void foo(int& x);

int y;
foo(y = 3);

который установит y в 3, а затем вызовет foo. Это было бы невозможно по вашему предложению. Конечно, вы можете утверждать, что y = 3; foo(y); в любом случае понятнее, но это скользкий путь: возможно, операторы приращения не должны быть разрешены внутри больших выражений и т. д. и т. д.

person M.M    schedule 23.08.2017