Шаблонный делегирующий конструктор копирования в постоянных выражениях

Этот вопрос мотивирован этот.

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

struct B {};

struct S {
    B b; // #1

    S() = default;

    template <typename ...dummy> // #2
    constexpr S(const S&) {}

    template <typename ...dummy> // #3
    constexpr S(S &other) 
        : S(const_cast<const S&>(other)) // #4
    {}
};

S s;
constexpr S f() {return s;}

int main() {
    constexpr auto x = f();
}

GCC успешно компилирует этот код, но Clang отклоняет его (Пример на Godbolt.org). Сообщение об ошибке, выдаваемое Clang:

<source>:21:20: error: constexpr variable 'x' must be initialized by a constant expression
    constexpr auto x = f();
                   ^   ~~~
<source>:13:11: note: read of non-constexpr variable 's' is not allowed in a constant expression
        : S(const_cast<const S&>(other)) 
          ^
<source>:13:11: note: in call to 'S(s)'
<source>:18:25: note: in call to 'S(s)'
constexpr S f() {return s;}
                        ^
<source>:21:24: note: in call to 'f()'
    constexpr auto x = f();
                       ^
<source>:17:3: note: declared here
S s;
  ^

Обратите внимание: если мы удалим любой из # 2, # 3 или # 4, оба компилятора примут этот код. Если мы заменим # 1 на int b = 0;, оба компилятора отклонят его.

У меня вопрос:

  1. Какой компилятор правильный в соответствии с действующим стандартом?
  2. Если GCC верен, почему замена # 1 на int b = 0; делает этот код некорректным? Если Clang верен, почему удаление любого из пунктов №2, №3 или №4 делает этот код правильным?

person xskxzr    schedule 01.03.2020    source источник
comment
Кстати, ни одна из этих функций не является конструктором копирования. По определению конструктор является конструктором копирования, только если он не является шаблоном.   -  person Nicol Bolas    schedule 01.03.2020
comment
@NicolBolas Спасибо, что указали на это. Я использую в заголовке только конструктор копирования, чтобы резюмировать проблему. Если у вас есть лучший заголовок, пожалуйста, отредактируйте его.   -  person xskxzr    schedule 01.03.2020


Ответы (1)


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

Таким образом, часть 1 сводится к следующей отличительной программе:

struct A {
    struct B {} b;
    constexpr A() {};
    // constexpr A(A const& a) : b{a.b} {}    // #1
};
int main() {
    auto a = A{};
    constexpr int i = (A{a}, 0);
}

Отклонено Clang и MSVC, принято gcc; раскомментируйте #1, чтобы все трое приняли.

Согласно определению неявно определенного конструктора копирования не существует Таким образом, #1 отличается от constexpr A(A const&) = default;, поэтому gcc верен. Также обратите внимание, что если мы дадим B определяемый пользователем конструктор копирования constexpr, Clang и MSVC снова примут, поэтому проблема, по-видимому, заключается в том, что эти компиляторы не могут отслеживать конструктивность копирования constexpr рекурсивно пустых, неявно копируемых классов. Зарегистрированные ошибки для MSVC и Clang (исправлено для Clang 11).

Часть 2:

Удаление #1 означает, что вы копируете (выполняете преобразование lvalue-to-rvalue) объект s.b типа int, время существования которого началось вне контекста constexpr.

Удаление #2 дает S определяемый пользователем constexpr конструктор копирования, который затем делегируется на #4.

Удаление #3 дает S определяемый пользователем (неконстантный) конструктор копирования, подавляя неявно определенный конструктор копирования, поэтому делегирующая конструкция вызывает конструктор шаблона const (который, помните, не является конструктором копирования).

Удаление #4 означает, что ваш шаблон конструктора с аргументом S& other больше не вызывает неявно определенный конструктор копирования, поэтому b инициализируется по умолчанию, что Clang может делать в контексте constexpr. Обратите внимание, что конструктор копирования по-прежнему неявно объявлен и определен как заданный по умолчанию, просто ваш конструктор template<class...> S::S(S& other) предпочтительнее с точки зрения разрешения перегрузки.

Важно понимать различие между подавлением неявно определенного конструктора копирования и предоставлением предпочтительной перегрузки. template<class...> S::S(S&) не подавляет неявно определенный конструктор копирования, но предпочтительнее для неконстантного аргумента lvalue, предполагая, что неявно определенный конструктор копирования имеет аргумент S const&. С другой стороны, template<class...> S::S(S const&) не подавляет неявно определенный конструктор копирования и никогда не может быть предпочтительнее неявно определенного конструктора копирования, поскольку это шаблон и списки параметров одинаковы.

person ecatmur    schedule 02.03.2020
comment
Не могли бы вы подробнее рассказать о рекурсивно пустых, неявно копируемых классах, что вы имеете в виду под рекурсивно пустыми? - person xskxzr; 03.03.2020
comment
@xskxzr под рекурсивно пустым я имею в виду неполиморфный тип класса, все нестатические элементы данных и непосредственные базы которого рекурсивно пусты. Другими словами, в нем нигде нет скалярных объектов, поэтому его представление значения пусто - оно не имеет состояния. Под неявно копируемым я подразумеваю, что его конструктор копирования не предоставляется пользователем, и все его непосредственные базы могут неявно копироваться, поэтому его конструктор копирования не вызывает какой-либо пользовательский код. (1/2) - person ecatmur; 03.03.2020
comment
Соедините их вместе, и вы получите класс, конструктор копирования которого полностью не работает, поэтому должно быть нормально вызывать в контексте constexpr, даже если время жизни исходного объекта началось вне контекста. (2/2) - person ecatmur; 03.03.2020