Должен ли постоянный С++ быть постоянным?

У меня есть класс С++, который имеет следующий интерфейс:

class F {
public:
    F(int n, int d);
    // no other constructors/assignment constructors defined
    F& operator *= (const F&);
    F& operator *= (int);
    int n() const;
    int d() const;
};

И у меня есть следующий код:

const F a{3, 7};
const F b{5, 10};
auto result = F{a} *= b; // How does this compile?

В Visual Studio (VS) 2013 комментированная строка компилируется без ошибок. В VS2015 создается ошибка C2678:

error C2678: binary '*=': no operator found 
    which takes a left-hand operand of type 'const F' 
    (or there is no acceptable conversion)
note: could be 'F &F::operator *=(const F &)'
note: or       'F &F::operator *=(int)'
note: while trying to match the argument list '(const F, const F)'

Я ожидал, что F{a} создаст неконстантную временную копию a, к которой будет применен operator *= (b), после чего временному объекту будет присвоено значение result. Я не ожидал, что временное будет постоянным. Интересно: auto result = F(a) *= b; компилируется без ошибок в VS2015, который, как я думал, должен быть семантически одинаковым.

Мой вопрос: какое поведение верно VS2015 или VS2013 и почему?

Большое спасибо

Ответы

Ответ 1

Visual Studio 2015 не создает правильный результат для:

F{a}

Результат должен быть prvalue (gcc и clang оба имеют этот результат), но он производит lvalue. Для получения этого результата я использую следующую измененную версию OP-кода:

#include <iostream>

class F {
public:
    F(int n, int d) :n_(n), d_(d) {};
    F(const F&) = default ;
    F& operator *= (const F&){return *this; }
    F& operator *= (int) { return *this; }
    int n() const { return n_ ; }
    int d() const { return d_ ; }
    int n_, d_ ;
};

template<typename T>
struct value_category {
    static constexpr auto value = "prvalue";
};

template<typename T>
struct value_category<T&> {
    static constexpr auto value = "lvalue";
};

template<typename T>
struct value_category<T&&> {
    static constexpr auto value = "xvalue";
};

#define VALUE_CATEGORY(expr) value_category<decltype((expr))>::value

int main()
{
  const F a{3, 7};
  const F b{5, 10};   
  std::cout << "\n" <<  VALUE_CATEGORY( F{a} ) <<  "\n";
}

Совет о шляпе Люк Дантон для кода VALUE_CATEGORY().

Visual Studio с использованием webcompiler, который имеет относительно новую версию:

lvalue

который должен быть const в этом случае, чтобы вызвать ошибку, которую мы видим. Хотя gcc и clang (см. Его в прямом эфире) производят:

prvalue

Это может быть связано с одинаково загадочной ошибкой Visual Studio std:: move строкового литерала - какой компилятор верен?.

Обратите внимание, что мы можем получить ту же проблему с gcc и clang, используя const F:

using cF = const F ;
auto result = cF{a} *= b; 

поэтому Visual Studio не только дает нам неправильную категорию значений, но также произвольно добавляет cv-квалификатор.

Как отметил Ханс в своих комментариях к вашему вопросу, используя F(a), вы получите ожидаемые результаты, так как он корректно создает prvalue.

Соответствующим разделом проекта стандарта С++ является раздел 5.2.3 [expr.type.conv], в котором говорится:

Аналогично, спецификатор simple-type-specifier или typename-specifier, за которым следует список braced-init-list, создает временную объект указанного типа с прямым списком-инициализацией (8.5.4) с указанным скобками-init-list и его значение равно этот временный объект как prvalue.

Заметьте, насколько я могу судить, это не старая ошибка MSVC lvalue cast. Решение этой проблемы заключается в использовании /Zc:rvalueCast, которая не устраняет эту проблему. Эта проблема также отличается неправильным добавлением cv-квалификатора, который, насколько я знаю, не происходит с предыдущей проблемой.

Ответ 2

Я думаю, что это ошибка в VS2015, потому что если вы укажете пользовательский конструктор копирования:

F(const F&);

или сделать переменную a неконтактный код будет скомпилирован успешно.

Похоже, что константа объекта из a переносится во вновь созданный объект.

Ответ 4

Из http://en.cppreference.com/w/cpp/language/copy_elision:

В следующих случаях компиляторам разрешено copy-and move-constructors объектов класса, даже если copy/move constructor и деструктор имеет наблюдаемые побочные эффекты.

.......

Когда безымянный временный, не привязанный к каким-либо ссылкам, будет перемещен или скопирован в объект того же типа (игнорирование верхнего уровня cv - квалификация), копирование/перемещение опущено. Когда это временное он построен непосредственно в хранилище, где он будет иначе перемещать или копировать. Когда безымянный временный аргумент возвращаемого утверждения, этот вариант копирования элиции известен как RVO, "оптимизация возвращаемого значения".

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