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


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.