Когда частный конструктор не является частным конструктором?
Допустим, у меня есть тип, и я хочу сделать его конструктор по умолчанию частным. Я пишу следующее:
class C {
C() = default;
};
int main() {
C c; // error: C::C() is private within this context (g++)
// error: calling a private constructor of class 'C' (clang++)
// error C2248: 'C::C' cannot access private member declared in class 'C' (MSVC)
auto c2 = C(); // error: as above
}
Отлично.
Но потом конструктор оказывается не таким приватным, как я думал:
class C {
C() = default;
};
int main() {
C c{}; // OK on all compilers
auto c2 = C{}; // OK on all compilers
}
Это кажется мне очень удивительным, неожиданным и явно нежелательным поведением. Почему это нормально?
2 ответа
Хитрость в C++ 14 8.4.2 / 5 [dcl.fct.def.default]:
... Функция предоставляется пользователем, если она объявлена пользователем и не имеет явных значений по умолчанию или удалена в первом объявлении....
Который означает, что C
Конструктор по умолчанию фактически не предоставляется пользователем, потому что он был явно установлен по умолчанию в своем первом объявлении. В качестве таких, C
не имеет пользовательских конструкторов и поэтому является агрегатом по 8.5.1/1 [dcl.init.aggr]:
Агрегат - это массив или класс (раздел 9) без предоставленных пользователем конструкторов (12.1), без закрытых или защищенных нестатических элементов данных (пункт 11), без базовых классов (пункт 10) и без виртуальных функций (10.3).
Вы не вызываете конструктор по умолчанию, вы используете агрегатную инициализацию для агрегатного типа. Агрегатным типам разрешено иметь конструктор по умолчанию, если он по умолчанию установлен там, где он был впервые объявлен:
Агрегат - это массив или класс (раздел [класс]) с
- нет пользовательских конструкторов ([class.ctor]) (включая наследуемые ([namespace.udecl]) от базового класса),
- нет частных или защищенных нестатических членов данных (пункт [class.access]),
- нет виртуальных функций ([class.virtual]), и
- нет виртуальных, частных или защищенных базовых классов ([class.mi]).
Явно-дефолтные функции и неявно-объявленные функции вместе называются дефолтными функциями, и реализация должна предоставлять для них неявные определения ([class.ctor] [class.dtor], [class.copy]), что может означать их определение как удаленных, Функция предоставляется пользователем, если она объявлена пользователем и не имеет явных значений по умолчанию или удалена в первом объявлении. Предоставленная пользователем явно дефолтная функция (т. Е. Явно дефолтная после ее первого объявления) определяется в точке, где она явно дефолтна; если такая функция неявно определена как удаленная, программа является некорректной. [Примечание: Объявление функции по умолчанию после ее первого объявления может обеспечить эффективное выполнение и краткое определение при одновременном включении стабильного двоичного интерфейса в развивающуюся базу кода. - конец примечания]
Таким образом, наши требования к совокупности:
- нет непубличных участников
- нет виртуальных функций
- нет виртуальных или закрытых базовых классов
- нет пользовательских конструкторов, унаследованных или иным образом, что допускает только те конструкторы, которые:
- неявно объявлено, или
- явно объявлен и определен как дефолтный одновременно.
C
выполняет все эти требования.
Естественно, вы можете избавиться от этого ложного поведения конструкции по умолчанию, просто предоставив пустой конструктор по умолчанию или определив конструктор по умолчанию после его объявления:
class C {
C(){}
};
// --or--
class C {
C();
};
inline C::C() = default;
Ответы Angew и jaggedSpire превосходны и применимы к С ++ 11. И C++14. И C++17.
Однако в С ++20 все немного изменилось, и пример в OP больше не будет компилироваться:
class C {
C() = default;
};
C p; // always error
auto q = C(); // always error
C r{}; // ok on C++11 thru C++17, error on C++20
auto s = C{}; // ok on C++11 thru C++17, error on C++20
Как указано в двух ответах, причина, по которой последние два объявления работают, заключается в том, что C
является агрегатом, и это инициализация агрегата. Однако в результате P1008 (используя мотивирующий пример, не слишком отличающийся от OP) определение агрегата в C++20 меняется на [dcl.init.aggr] / 1:
Агрегат - это массив или класс ([class]) с
- нет объявленных пользователем или унаследованных конструкторов ([class.ctor]),
- нет частных или защищенных прямых нестатических членов данных ([class.access]),
- нет виртуальных функций ([class.virtual]) и
- нет виртуальных, частных или защищенных базовых классов ([class.mi]).
Акцент мой. Теперь требованием является отсутствие конструкторов, объявленных пользователем, тогда как раньше (как оба пользователя цитируют в своих ответах и могут быть просмотрены исторически для C++11, C++14 и C++17) не было конструкторов, предоставляемых пользователем.. Конструктор по умолчанию дляC
объявляется пользователем, но не предоставляется пользователем и, следовательно, перестает быть агрегатом в C++20.
Вот еще один наглядный пример совокупных изменений:
class A { protected: A() { }; };
struct B : A { B() = default; };
auto x = B{};
B
не был агрегатом в C++11 или C++14, потому что у него есть базовый класс. Как результат,B{}
просто вызывает конструктор по умолчанию (объявленный пользователем, но не предоставленный пользователем), который имеет доступ к A
Защищенный конструктор по умолчанию.
В C++17 в результате P0017 агрегаты были расширены для поддержки базовых классов.B
является агрегатом в C++17, что означает, что B{}
агрегатная инициализация, которая должна инициализировать все подобъекты, включая A
подобъект. Но потому чтоA
конструктор по умолчанию защищен, у нас нет доступа к нему, поэтому эта инициализация некорректна.
В C++20 из-за B
объявленный пользователем конструктор, он снова перестает быть агрегатом, поэтому B{}
возвращается к вызову конструктора по умолчанию, и это снова правильная инициализация.
Это работает, потому что вы звоните std::initializer_list<>
в конструкторе. Таким образом, не имеет значения, является ли ваш класс типом агрегации или нет, или это сложно c++14
или что-то типа того. Вы можете сделать следующее, чтобы в любом случае не создавать объект класса.
class C {
C() = delete;
C(const C&) = delete;
C(C&&) = delete;
C& operator=(const C&) = delete;
template<typename T>
C(std::initializer_list<T>) = delete;
};
int main() {
C c{}; // Not OK on all compilers
auto c2 = C{}; // Not OK on all compilers
}