Почему константа локальной переменной запрещает переносить семантику для возвращаемого значения?

struct STest : public boost::noncopyable {
    STest(STest && test) : m_n( std::move(test.m_n) ) {}
    explicit STest(int n) : m_n(n) {}
    int m_n;
};

STest FuncUsingConst(int n) {
    STest const a(n);
    return a;
}

STest FuncWithoutConst(int n) {
    STest a(n);
    return a;
}

void Caller() {
    // 1. compiles just fine and uses move ctor
    STest s1( FuncWithoutConst(17) );

    // 2. does not compile (cannot use move ctor, tries to use copy ctor)
    STest s2( FuncUsingConst(17) );
}

Приведенный выше пример иллюстрирует, как в С++ 11, реализованном в Microsoft Visual С++ 2012, внутренние детали функции могут изменять свой тип возврата. До сегодняшнего дня я понимал, что декларация типа возврата - все, что программист должен знать, чтобы понять, как будет обрабатываться возвращаемое значение, например, при передаче в качестве параметра для последующего вызова функции. Не так.

Мне нравится делать локальные переменные const, где это необходимо. Это помогает мне очистить мой ход мысли и четко структурировать алгоритм. Но остерегайтесь возврата переменной, объявленной const! Даже несмотря на то, что переменная больше не будет доступна (в конце была выполнена инструкция return), и даже несмотря на то, что переменная, которая была объявлена ​​const, уже давно вышла за рамки (оценка выражения параметра завершена), она не может быть перемещен и, следовательно, будет скопирован (или не скомпилирован, если копирование невозможно).

Этот вопрос связан с другим вопросом, Переместить семантику и вернуть значения const. Разница в том, что в последнем объявлена ​​функция, возвращающая значение const. В моем примере объявлено, что FuncUsingConst возвращает изменчивое временное значение. Тем не менее, результирующие детали тела функции влияют на тип возвращаемого значения и определяют, можно ли использовать возвращаемое значение в качестве параметра для других функций.

Является ли это поведение стандартным?
Как это можно считать полезным?

Бонусный вопрос: как компилятор может узнать разницу во время компиляции, учитывая, что вызов и реализация могут быть в разных единицах перевода?


EDIT: попытка перефразировать вопрос.

Как возможно, что результат функции больше, чем объявленный тип возврата? Как вообще кажется приемлемым, что объявление функции недостаточно для определения поведения возвращаемого значения функции? Для меня это похоже на FUBAR, и я просто не уверен, виноват ли стандарт или реализация Microsoft.

Как разработчик вызываемой функции, я не ожидаю, что даже узнаю всех абонентов, не говоря уже о том, чтобы отслеживать каждое небольшое изменение в вызывающем коде. С другой стороны, как исполнитель вызывающей функции, я не могу полагаться на вызываемую функцию, чтобы не возвращать переменную, которая, как считается, объявлена ​​const в рамках реализации функции.

Объявление функции - это контракт. Что это стоит? Мы не говорим о семантически эквивалентной оптимизации компилятора здесь, как copy elision, что приятно иметь, но не меняет смысл кода. Независимо от того, вызвана ли копия ctor, изменяется ли смысл кода (и может даже сломать код до такой степени, что он не может быть скомпилирован, как показано выше). Чтобы оценить неловкость того, что я здесь обсуждаю, рассмотрите "бонусный вопрос" выше.

Ответы

Ответ 1

Программа плохо сформирована, если конструктор copy/move [...] для объекта неявно используется odr и специальная функция-член недоступна

- n3485 С++ draft standard [class.copy]/30

Я подозреваю, что ваша проблема связана с MSVC 2012, а не с С++ 11.

Этот код, даже не вызывая его, не является законным С++ 11:

struct STest {
  STest(STest const&) = delete
  STest(STest && test) : m_n( std::move(test.m_n) ) {}
  explicit STest(int n) : m_n(n) {}
  int m_n;
};

STest FuncUsingConst(int n) {
  STest const a(n);
  return a;
}

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

Если MSVC2012 позволяет компилировать FuncUsingConst, он делает это в нарушение стандарта С++ 11.

Ответ 2

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

Это действительно хорошая практика. Используйте const везде, где сможете. Здесь, однако, вы не можете (если вы ожидаете, что ваш объект const будет удален).

Тот факт, что вы объявляете объект const внутри вашей функции, является обещанием, что ваше состояние объекта никогда не будет изменено до тех пор, пока объект будет жив, другими словами, никогда прежде, чем его деструктор будет вызван. Даже сразу перед вызовом деструктора. Пока он жив, состояние объекта const не изменяется.

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

Однако вы можете создать объект non const и получить доступ к нему в своей функции только с помощью ссылки на const, связанной с этим объектом:

STest FuncUsingConst(int n) {
    STest object_not_to_be_touched_if_not_through_reference(n);
    STest const& a = object_not_to_be_touched_if_not_through_reference;

    // Now work only with a

    return object_not_to_be_touched_if_not_through_reference;
}

С небольшим количеством дисциплины вы можете легко ввести в действие семантику, что функция не должна изменять этот объект после его создания, за исключением того, что ему разрешено перемещаться из него при возврате.

UPDATE:

Как было предложено в комментариях balki, другой возможностью было бы привязать постоянную ссылку на неконстантный временный объект (срок жизни которого был бы продлен как в соответствии с § 12.2/5), и при a const_cast при возврате:

STest FuncUsingConst(int n) {
    STest const& a = STest();

    // Now work only with a

    return const_cast<STest&&>(std::move(a));
}