Коли приватний конструктор не є приватним конструктором?


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

5
Що сказав @NathanOliver. У вас немає конструктора, наданого користувачем, тому Cє сукупним.
Kerrek SB

5
@KerrekSB У той же час для мене було досить дивно, що користувач, явно оголосивши ctor, не робить цей ctor користувачем.
Енджу більше не пишається SO

1
@Angew Ось чому ми всі тут :)
Баррі

2
@Angew Якби це був публічний =defaultктор, це здавалося б більш розумним. Але приватний =defaultктор здається важливою справою, яку не слід ігнорувати. Більше того, class C { C(); } inline C::C()=default;бути зовсім іншим дещо дивно.
Якк - Адам Неврамон

Відповіді:


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 ).


13
Фактично, невеликий стандартний дефект: той факт, що ctor за замовчуванням був приватним, фактично ігнорується в цьому контексті.
Якк - Адам Неврамон

2
@Yakk Я не відчуваю себе кваліфікованим, щоб судити про це. Формулювання про те, що ктор не надається користувачем, виглядає дуже обдуманим.
Енджу більше не пишається SO

1
@Yakk: Ну, так і ні. Якби в класі були члени даних, ви мали б можливість зробити їх приватними. Без учасників даних дуже мало ситуацій, коли ця ситуація серйозно зачепить когось.
Керрек СБ

2
@KerrekSB Це має значення, якщо ви намагаєтесь використовувати клас як свого роду "маркер доступу", контролюючи, наприклад, хто може викликати функцію на основі того, хто може створити об'єкт класу.
Енджу більше не пишається SO

5
@Yakk Ще цікавіше те, що C{}працює, навіть якщо конструктор deleted.
Barry

56

Ви не викликаєте конструктор за замовчуванням, ви використовуєте агрегатну ініціалізацію для агрегатного типу. У агрегованих типів дозволяється мати конструктор за замовчуванням, якщо він за замовчуванням там, де він вперше оголошений:

З [dcl.init.aggr] / 1 :

Сукупність - це масив або клас (Стаття [клас]) з

  • відсутність наданих користувачем конструкторів ([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

Відповіді Енджу та нерівного шпиля відмінні і стосуються. І. І.

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

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.