Когда частный конструктор не является частным конструктором?

92

Допустим, у меня есть тип, и я хочу сделать его конструктор по умолчанию закрытым. Я пишу следующее:

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
}    

Это кажется мне очень удивительным, неожиданным и явно нежелательным поведением. Почему это нормально?

Барри
источник
25
Разве C c{};агрегатная инициализация не вызывается без вызова конструктора?
NathanOliver 03
5
Что сказал @NathanOliver. У вас нет конструктора, предоставленного пользователем, поэтому Cесть агрегат.
Kerrek SB 03
5
@KerrekSB В то же время для меня было довольно удивительно, что пользователь, явно объявляющий ctor, не делает этот ctor предоставляемым пользователем.
Энгью больше не гордится SO
1
@Angew Вот почему мы все здесь :)
Барри
2
@Angew Если бы это был публичный =defaultклиент, это было бы более разумно. Но частный =defaultctor кажется важной вещью, которую нельзя игнорировать. Более class C { C(); } inline C::C()=default;того, это несколько удивительно.
Yakk - Adam Nevraumont 03

Ответы:

61

Уловка в 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 ).

Энгью больше не гордится SO
источник
13
По сути, небольшой стандартный недостаток: тот факт, что ctor по умолчанию был частным, в данном контексте фактически игнорируется.
Yakk - Adam Nevraumont 03
2
@Yakk Я не считаю себя вправе судить об этом. Однако формулировка о том, что ctor не предоставляется пользователем, выглядит очень обдуманной.
Энгью больше не гордится SO
1
@Yakk: Да и нет. Если бы у класса были какие-либо элементы данных, у вас была бы возможность сделать их закрытыми. Без членов данных очень мало ситуаций, когда эта ситуация кого-либо серьезно затронет.
Kerrek SB 03
2
@KerrekSB Это имеет значение, если вы пытаетесь использовать класс как своего рода «токен доступа», управляя, например, тем, кто может вызывать функцию в зависимости от того, кто может создать объект класса.
Энгью больше не гордится SO
5
@Yakk Еще более интересно то, что C{}работает, даже если конструктор deleted.
Barry
56

Вы не вызываете конструктор по умолчанию, вы используете агрегатную инициализацию для агрегатного типа. Агрегатным типам разрешено иметь конструктор по умолчанию, если он используется по умолчанию там, где он был впервые объявлен:

Из [dcl.init.aggr] / 1 :

Агрегат - это массив или класс (Clause [class]) с

  • никаких конструкторов, предоставленных пользователем ([class.ctor]) (включая унаследованные ([namespace.udecl]) от базового класса),
  • нет частных или защищенных нестатических элементов данных (пункт [class.access]),
  • нет виртуальных функций ([class.virtual]) и
  • нет виртуальных, частных или защищенных базовых классов ([class.mi]).

и из [dcl.fct.def.default] / 5

Явно заданные по умолчанию функции и неявно объявленные функции вместе называются функциями по умолчанию, и реализация должна предоставлять для них неявные определения ([class.ctor] [class.dtor], [class.copy]), что может означать определение их как удаленных . Функция предоставляется пользователем, если она объявлена ​​пользователем и явно не задана по умолчанию или не удалена при первом объявлении. Предоставляемая пользователем функция с явно заданным по умолчанию значением (т. Е. Явно заданная по умолчанию после первого объявления) определяется в точке, где она явно задана по умолчанию; если такая функция неявно определена как удаленная, программа имеет неправильный формат.[Примечание: объявление функции по умолчанию после ее первого объявления может обеспечить эффективное выполнение и краткое определение, одновременно обеспечивая стабильный двоичный интерфейс с развивающейся базой кода. - конец примечания]

Таким образом, наши требования к агрегату:

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

C выполняет все эти требования.

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

class C {
    C(){}
};
// --or--
class C {
    C();
};
inline C::C() = default;
зазубренный
источник
2
Мне этот ответ несколько больше нравится, чем ответ Ангью, но я думаю, что было бы полезно, если краткое изложение в начале состоит максимум из двух предложений.
PJTraill
7

Ответы Angew и jaggedSpire превосходны и применимы к. А также. А также.

Однако в , все немного изменится, и пример в 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{}возвращается к вызову конструктора по умолчанию, и это снова правильно сформированная инициализация.

Барри
источник