Почему агрегатная инициализация больше не работает с С++ 20, если конструктор явно дефолтен или удален?

Я перевожу проект Visual Studio C++ с VS2017 на VS2019.

Сейчас я получаю сообщение об ошибке, которого раньше не было, которое можно воспроизвести с помощью нескольких строк кода:

struct Foo
{
    Foo() = default;
    int bar;
};
auto test = Foo { 0 };

Ошибка

(6): ошибка C2440: "инициализация": невозможно преобразовать из 'список инициализатора' в 'Foo'

(6): примечание: ни один конструктор не может взять тип источника, или Разрешение перегрузки конструктора было неоднозначным

Проект скомпилирован с флагом /std:c++latest. Я воспроизвел это на Годболте. Если я переключаю его на /std:c++17, он прекрасно компилируется, как и раньше.

Я попытался скомпилировать тот же код с clang с -std=c++2a и получил похожую ошибку. Кроме того, по умолчанию или удаление других конструкторов генерирует эту ошибку.

По-видимому, некоторые новые функции C++ 20 были добавлены в VS2019, и я предполагаю, что происхождение этой проблемы описано в https://en.cppreference.com/w/cpp/language/aggregate_initialization. Там говорится, что агрегат может быть структурой, которая (среди прочих критериев) имеет

  • не предоставленные пользователем, унаследованные или явные конструкторы (конструкторы по умолчанию или удаленные по умолчанию не допускаются) (начиная с C++ 17) (до C++ 20)
  • нет объявленных или наследуемых конструкторов (начиная с C++ 20)

Обратите внимание, что часть в скобках "явно заданные по умолчанию или удаленные конструкторы разрешены" была удалена, а "предоставленная пользователем" заменена на "объявленная пользователем".

Итак, мой первый вопрос: правильно ли я полагаю, что это изменение в стандарте является причиной, по которой мой код компилировался раньше, но больше этого не делает?

Конечно, это легко исправить: просто удалите явно дефолтные конструкторы.

Тем не менее, я явно по умолчанию и удалил очень много конструкторов во всех моих проектах, потому что я нашел хорошей привычкой делать код гораздо более выразительным таким образом, потому что это просто приводит к меньшему количеству сюрпризов, чем с неявно дефолтными или удаленными конструкторами. Однако с этим изменением это уже не кажется такой хорошей привычкой...

Итак, мой актуальный вопрос: Каковы причины этого изменения с C++ 17 на C++ 20? Был ли этот разрыв обратной совместимости сделан специально? Был ли какой-то компромисс, такой как "Хорошо, мы нарушаем обратную совместимость здесь, но это для большей пользы"? Что это больше хорошего?

Ответ 1

Резюме от P1008, предложение, которое привело к изменению:

C++ в настоящее время позволяет инициализировать некоторые типы с объявленными пользователем конструкторами через агрегатную инициализацию, минуя эти конструкторы. В результате получается удивительный, запутанный и ошибочный код. В этой статье предлагается исправление, которое делает семантику инициализации в C++ более безопасной, унифицированной и удобной для обучения. Мы также обсудим важные изменения, которые вносит это исправление.

Один из примеров, которые они приводят, следующий:

struct X {
  int i{4};
  X() = default;
};

int main() {
  X x1(3); // ill-formed - no matching ctor
  X x2{3}; // compiles!
}

Для меня совершенно ясно, что предлагаемые изменения стоят той несовместимости, которую они несут. И действительно, кажется, что больше не рекомендуется использовать = default агрегатные конструкторы по умолчанию.

Ответ 2

Рассуждения из P1008 (PDF) лучше всего понять с двух сторон:

  1. Если вы поставите относительно нового программиста C++ перед определением класса и спросите: "Является ли это совокупностью", они будут правильными?

Распространенной концепцией агрегата является "класс без конструкторов". Если Typename() = default; находится в определении класса, большинство людей увидят в нем конструктор. Он будет вести себя как стандартный конструктор по умолчанию, но у типа все еще есть. Это широкая концепция идеи от многих пользователей.

Предполагается, что агрегат представляет собой класс чистых данных, способный заставить любого члена принимать любое значение, которое ему дано. С этой точки зрения, вы не имеете права давать ему конструкторы любого рода, даже если вы их не выполняете. Что приводит нас к следующему рассуждению:

  1. Если мой класс удовлетворяет требованиям агрегата, но я не хочу, чтобы он был агрегатом, как мне это сделать?

Наиболее очевидным ответом будет = default конструктор по умолчанию, потому что я, вероятно, кто-то из группы # 1. Очевидно, что это не работает.

До C++ 20 ваши варианты заключаются в том, чтобы дать классу какой-либо другой конструктор или реализовать одну из специальных функций-членов. Ни один из этих вариантов не является приемлемым, потому что (по определению) это не то, что вам действительно нужно реализовать; вы просто делаете это, чтобы вызвать побочный эффект.

После C++ 20 очевидный ответ работает.

Изменяя правила таким образом, вы видите разницу между агрегатом и неагрегатом. Агрегаты не имеют конструкторов; поэтому, если вы хотите, чтобы тип был агрегатом, вы не предоставляете его конструкторам.

О, и вот забавный факт: до C++ 20 это совокупность:

class Agg
{
  Agg() = default;
};

Обратите внимание, что конструктор по умолчанию является закрытым, поэтому вызывать его могут только люди, имеющие частный доступ к Agg... если только они не используют Agg{}, обходят конструктор и являются совершенно законными.

Явное намерение этого класса - создать класс, который можно копировать, но который может получить его первоначальную конструкцию только из тех, кто имеет частный доступ. Это позволяет переадресовывать элементы управления доступом, поскольку только код, которому был присвоен Agg, может вызывать функции, которые принимают Agg в качестве параметра. И только код с доступом к Agg может создать его.

Или, по крайней мере, так, как это должно быть.

Теперь вы можете исправить это более целенаправленно, сказав, что это агрегат, если конструкторы по умолчанию/удаленные не объявлены публично. Но это кажется еще более неконгруэнтным; иногда класс с явно объявленным конструктором является агрегатом, а иногда нет, в зависимости от того, где находится этот явно объявленный конструктор.

Ответ 3

На самом деле, MSDN решил вашу проблему в следующем документе:

Модифицированная спецификация типа агрегата

В Visual Studio 2019 в /std: c ++ latest класс с любым объявленным пользователем конструктором (например, включая конструктор, объявленный = default или = delete) не является агрегатом. Ранее только предоставленные пользователем конструкторы не допускали, чтобы класс был агрегатом. Это изменение накладывает дополнительные ограничения на то, как такие типы могут быть инициализированы.