Почему «нормально»? Реализации функций копирования / перемещения в производных классах ведут себя по-разному в зависимости от того, как они определены?

Я запутался в поведении, которое я вижу, когда функции копирования и перемещения производного класса вызывают свои версии базового класса.

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

#include <iostream>

class Base {
public:
Base() {}

template<typename T>
Base(T&&) { std::cout << "URef ctor\n"; }

Base(const Base&) { std::cout << "Copy ctor\n"; }

Base(Base& rhs): Base(const_cast<const Base&>(rhs))
{ std::cout << "  (from non-const copy ctor)\n"; }

Base(Base&&) { std::cout << "Move ctor\n"; }

Base(const Base&& rhs): Base(rhs)
{ std::cout << "  (from const move ctor)\n"; }
};

Для производного класса с созданными компилятором операциями копирования и перемещения

class Derived: public Base {};

и этот тестовый код,

int main()
{
Derived d;
Derived copyNCLValue(d);
Derived copyNCRvalue(std::move(d));

const Derived cd;
Derived copyCLValue(cd);
Derived copyCRvalue(std::move(cd));
}

GCC 4.8.1 производит этот вывод:

Copy ctor
Move ctor
Copy ctor
Copy ctor

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

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

class Derived: public Base {
public:
Derived(){}

Derived(const Derived& rhs) = default;
Derived(Derived&& rhs) = default;
};

GCC выдает тот же результат. Но если я сам напишу функции, используя то, что считаю версией по умолчанию,

class Derived: public Base {
public:
Derived(){}

Derived(const Derived& rhs): Base(rhs) {}
Derived(Derived&& rhs): Base(std::move(rhs)) {}
};

Я получаю вывод, который первоначально ожидал:

URef ctor
URef ctor
URef ctor
URef ctor

Я ожидаю получить одинаковый результат в каждом случае. Это ошибка в gcc, или я чего-то не понимаю?

1

Решение

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

Компилятор видит строку Derived copyCRvalue(std::move(cd)); это действительно значит Derived copyCRvalue(static_cast<const Derived&&>(cd)); и он пытается найти конструктор в Derived это соответствует этому вызову. Он находит два тесно связанных конструктора, оба из которых неявно объявлены:

Derived(Derived const &); // copy constructor
Derived(Derived &&);      // move constructor

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

Derived(Derived const &rhs) : base(static_cast<Base const &>(rhs)) {}

Внутри конструктора rhs является именующий, не rvalue (и шаблонный конструктор в любом случае не является конструктором копирования).

Но если я сам напишу функции, используя то, что считаю версией по умолчанию,

class Derived: public Base {
public:
Derived(){}

Derived(const Derived& rhs): Base(rhs) {}
Derived(Derived&& rhs): Base(std::move(rhs)) {}
};

За исключением того, что это не те определения, которые предоставит компилятор. Конкретная квота из стандарта в 12,8 / 15

Неявно определенный конструктор копирования / перемещения для класса X, не являющегося объединением, выполняет пошаговое копирование / перемещение своих баз и членов.

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

6

Другие решения

Других решений пока нет …