Почему класс, имеющий закрытый конструктор, не предотвращает наследование от этого класса? Как контролировать, какие классы могут наследоваться от определенной базы?

class B {
private:
    friend class C;
    B() = default;
};

class C : public B {};
class D : public B {};

int main() {
    C {};
    D {};
    return 0;
}

Я предположил, что, поскольку только класс C является другом B, а конструктор B является закрытым, то действительным является только класс C а D не может создавать экземпляр B Но это не так, как это работает. Где я ошибаюсь в своих рассуждениях и как добиться такого контроля над тем, какие классы могут наследовать определенную базу?

Обновление: как отмечалось другими в комментариях, приведенный выше фрагмент работает так, как я первоначально ожидал в С++ 14, но не в С++ 17. Изменение экземпляра на C c; D d; C c; D d; Функция main() также работает, как и ожидалось, в режиме С++ 17.

Ответ 1

Это новая функция, добавленная в С++ 17. То, что происходит, - это C, теперь считается совокупностью. Поскольку это агрегат, ему не нужен конструктор. Если мы посмотрим на [dcl.init.aggr]/1, то получим, что агрегат

Агрегат - это массив или класс с

  • нет пользовательских, явных или унаследованных конструкторов ([class.ctor]),

  • нет частных или защищенных нестатических членов данных (пункт [class.access]),

  • нет виртуальных функций, и

  • нет виртуальных, частных или защищенных базовых классов ([class.mi]).

[Примечание: Агрегированная инициализация не позволяет получить доступ к защищенным и закрытым членам или конструкторам базового класса. - конец примечания]

И мы проверяем все эти пункты пули. У вас нет конструкторов, объявленных в C или D поэтому есть пункт 1. У вас нет элементов данных, поэтому второй пункт не имеет значения, а ваш базовый класс общедоступен, поэтому третий пункт удовлетворен.

Изменения, произошедшие между С++ 11/14 и С++ 17, которые позволяют это, состоят в том, что агрегаты теперь могут иметь базовые классы. Вы можете увидеть старую формулировку здесь, где прямо указано, что базовые классы не допускаются.

Мы можем подтвердить это, проверив черту std::is_aggregate_v как

int main()
{
    std::cout << std::is_aggregate_v<C>;
}

который напечатает 1.


Обратите внимание, что, поскольку C является другом B вы можете использовать

C c{};
C c1;
C c2 = C();

Как действительные способы инициализации C Поскольку D не друг B единственный, кто работает, это D d{}; как это совокупная инициализация. Все другие формы пытаются инициализировать по умолчанию, и это не может быть сделано, так как D имеет удаленный конструктор по умолчанию.

Ответ 2

Из Что такое доступ по умолчанию для конструктора в c++:

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

Если определение класса не объявляет явно конструктор копирования, он объявляется неявно. [...] Неявно объявленный конструктор копирования/перемещения является встроенным открытым членом своего класса.

Конструкторы для классов C и D генерируются внутри компилятором.

Кстати: если вы хотите играть с наследованием, убедитесь, что у вас определен виртуальный деструктор.