Идеальная пересылка в конструкторах (С++ 17)

Рассмотрим следующий код

struct A {
    A(int id) : id_ { id } {}

    A(const A& rhs) { std::cout << "cctor from " +
        std::to_string(rhs.id_) << std::endl; }
    A(A&& rhs) { std::cout << "mctor from " +
        std::to_string(rhs.id_) << std::endl; }

    int id_;
};

template<typename T>
struct B1 {
    constexpr B1(T&& x) noexcept : x_ { std::forward<T>(x) } {}

    T x_;
};

template<typename T>
struct B2 {
    constexpr B2(T&& x) noexcept;

    T x_;
};

template<typename T>
constexpr
B2<T>::B2(
    T&& x
) noexcept :
    x_ { std::forward<T>(x) } {
}

int
main(
) {
    A a { 1 };

    //B1 b11 { a }; // Not compiling
    B1 b12 { A { 2 } };

    B2 b21 { a };
    B2 b22 { A { 3 } };

    return 0;
 }

который дает

mctor from 2
mctor from 3

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

Это то, что внешне определенный конструктор обрабатывается как шаблон функции (который прекрасно передает свои аргументы) или что здесь происходит?

Ссылки на соответствующий раздел стандарта приветствуются.

Я использую GCC 7.2.0.

Ответы

Ответ 1

Это ошибка GCC. Пересылочные ссылки имеют очень четкое определение:

[temp.deduct.call] (выделено мое)

3 Ссылка на пересылку - это rvalue-ссылка на cv-неквалифицированный параметр шаблона , который не представляет параметр шаблона шаблона класса (во время вывода аргумента шаблона класса ([over.match.class.deduct])). Если P является ссылкой для пересылки, а аргумент является lvalue, тип "lvalue ссылка на A" используется вместо A для вывода типа.

В обоих случаях T называет параметр шаблона класса включения во время CTAD, поэтому он не должен создавать ссылку для пересылки в любом случае. C'or, определяемый внутри или вне определения класса, не имеет к этому никакого отношения.

Ответ 2

Похоже, что GCC неправильно обрабатывает T&& в автоматически сгенерированном руководстве по вычетам как ссылку для пересылки:

template <typename T>
B2(T&& x) -> B2<T>;

В этом случае T&& - это неоправданная ссылка на r-значение, потому что это параметр класса. Вместо этого GCC неправильно выводит тип параметра T=A& и тип класса B2<T>=B2<A&> <A &>, что приводит к сворачиванию ссылочного типа в конструкторе, что позволяет скомпилировать код с аргументом конструктора lvalue:

constexpr B2(A& x) noexcept;

Вывод аргумента шаблона класса не делает различий между встроенными и внешними определениями. В данном конкретном случае B2 b21 { a }; должен потерпеть неудачу.