Подтвердить что ты не робот

Перемещение с одной стороны тройного оператора

Я писал код, похожий на:

std::string foo(bool b, const std::string& fst, std::string&& snd) {
  return b ? fst : std::move(snd);
}

и clang и скопировал snd, а gcc переместил его. Я попытался свести к минимуму пример, и я придумал:

#include <iostream>
#include <utility>

struct printer {
  printer() { }
  printer(const printer&) { std::cout << "copy" << std::endl; }
  printer(printer&&) { std::cout << "move" << std::endl; }
  printer(const printer&&) { std::cout << "const rvalue ref" << std::endl; }
};

int main() {
  const printer fst;
  printer snd;
  false ? fst : std::move(snd);
}

gcc 5.2 выходы

move

clang 3.6 выходы

const rvalue ref

Поддерживает ли стандарт как поведение gcc, так и clang?

Случайные наблюдения ниже:

Оба gcc и clang унифицируют тип тройки:

const printer

Разборка gcc 5.2

clang 3.6 disassembly

4b9b3361

Ответ 1

Тип std::move(x)

Итак, давайте начнем с определения типа std::move(snd). Реализация std::move(x) определяется как приблизительно static_cast<T&&>(x), согласно §20.2.4:

template <class T> constexpr remove_reference_t<T>&& move(T&& t) noexcept;

Возвращает: static_cast<remove_reference_t<T>&&>(t).

который согласно §5.2.9/1:

Результат выражения static_cast<T>(v) является результатом преобразования выражения v в тип T. Если T - ссылочный тип lvalue или ссылочная позиция rvalue для типа функции, результатом является lvalue; , если T является ссылкой rvalue на тип объекта, результатом является xvalue; в противном случае результатом будет prvalue. Оператор static_cast не должен отбрасывать константу (5.2.11).

(акцент мой)

Итак, возвращаемое значение std::move(snd) является значением x типа printer&&. Тип fst - это значение типа const printer.


Как рассчитать общий тип

Теперь стандарт описывает процесс вычисления типа полученного выражения для тернарного условного оператора:

если второй и третий операнды имеют разные типы и либо имеют (возможно, cv-qualit) тип класса, либо оба значения glvalues ​​одной и той же категории значений и того же типа, за исключением cv-qualification, делается попытка конвертировать каждый из этих операндов относится к типу другого. Процесс определения того, можно ли преобразовать выражение E1 операнда типа T1 в соответствие с выражением операнда E2 типа T2, определяется следующим образом:

  • Если E2 является lvalue: E1 может быть преобразован в соответствие E2, если E1 может быть неявно преобразован (раздел 4) в тип "lvalue reference to T2", при условии ограничения, которое при преобразовании ссылка должна связываться напрямую ( 8.5.3) к значению l.
  • Если E2 является значением x: E1 может быть преобразован в соответствие E2, если E1 может быть неявно преобразован в тип "rvalue reference to T2", при условии ограничения, которое ссылка должна связываться напрямую.
  • Если E2 является prvalue или если ни одно из приведенных выше преобразований не может быть выполнено, и хотя бы один из операндов имеет (возможно, cv-qualified) тип класса:

    • если E1 и E2 имеют тип класса, а базовые типы классов одинаковы или один является базовым классом другого: E1 может быть преобразован в соответствие E2, если класс T2 является тем же типом, что и базовый класс класс, класс T1 и cv-квалификация T2 - это та же самая cv-квалификация, что и более высокая cv-квалификация, чем cv-квалификация T1. Если преобразование применяется, E1 заменяется на prvalue типа T2 путем копирования-инициализации временного типа T2 из E1 и использования этого временного в качестве преобразованного операнда.
    • В противном случае (если E1 или E2 имеет тип неклассов, или если оба типа имеют типы классов, но базовые классы не являются одинаковыми, и ни один из них не является базовым классом другого): E1 может быть преобразован в соответствие E2, если E1 может быть неявно преобразован в тип, который E2 будет иметь после применения lvalue-to- rvalue (4.1), преобразование от массива к указателю (4.2) и стандартное преобразование функции-to-pointer (4.3).

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

(опять же мой удар)


В этом контексте

Итак, у нас есть два случая:

  • E1 - fst, а E2 - std::move(snd)
  • E1 - std::move(snd), а E2 - fst

В первом случае мы имеем, что E2 является значением x, поэтому:

Если E2 является значением x: E1 может быть преобразован в соответствие E2, если E1 может быть неявно преобразован в тип "rvalue reference to T2", при условии ограничения, которое ссылка должна связываться напрямую.

применяется; но E1 (типа const printer) не может быть неявно преобразован в printer&& из-за того, что он потеряет константу. Таким образом, это преобразование невозможно.

Во втором случае мы имеем:

Если E2 является lvalue: E1 может быть преобразован в соответствие E2, если E1 может быть неявно преобразован (раздел 4) в тип "lvalue reference to T2", при условии ограничения, которое при преобразовании ссылка должна привязываться напрямую ( 8.5.3) к значению l.

применяется, но E1 (std::move(snd) типа printer&&) может быть неявно преобразован в const printer&, но не привязывается непосредственно к lvalue; поэтому этот тоже не применяется.

В этот момент мы находимся:

Если E2 является prvalue или если ни одно из приведенных выше преобразований не может быть выполнено, и по крайней мере один из операндов имеет (возможно, cv-qualified) тип класса:

раздела.

Из этого следует рассмотреть:

если E1 и E2 имеют тип класса, а базовые типы классов являются одинаковыми или один является базовым классом другого: E1 может быть преобразован в соответствие E2, если класс T2 является тем же типом, что и базовая класс, класс T1 и cv-квалификация T2 - это та же самая cv-квалификация, что и более высокая cv-квалификация, чем cv-квалификация T1. Если применяется преобразование, E1 заменяется на prvalue типа T2 путем копирования-инициализации временного типа T2 из E1 и использования этого временного в качестве преобразованного операнда.

E1 и E2 действительно имеют одни и те же базовые типы классов. И const printer имеет большее cv-квалификацию, чем cv-квалификация std::move(snd), поэтому случай таков, что E1 = std::move(snd) и E2 = fst.

Из чего мы наконец получаем следующее:

E1 изменен на prvalue типа T2 путем копирования-инициализации временного типа T2 из E1 и использования этого временного в качестве преобразованного операнда.

Что переводится на то, что std::move(snd) изменено на prvalue типа const printer путем копирования-инициализации временного типа const printer из std::move(snd).

Так как std::move(snd) дает a printer&&, выражение будет эквивалентно построению a const printer с printer(std::move(snd)), что должно привести к выбору printer(printer&&).