Pull to refresh

Comments 38

Как же так, вы приводите ссылки, а сами, видимо, даже не удосужились их прочитать.
Да ещё и всё напутали… В С++17 в вашем примере:

  • auto x = {0}; // decltype(x) is std::initializer_list

    auto y = {1, 2}; // decltype(н) is std::initializer_list


    обычный int, будето только при прямой инициализации
    auto x5{ 3 }; // decltype(x5) is int

    Что будет, если я напишу auto x = {0}; auto y = {1, 2};? Можно придумать несколько разумных стратегий:

    Запретить такую инициализацию вообще (в самом деле, что программист хочет этим сказать?)
    Вывести тип первой переменной как int, а второй вариант запретить
    Сделать так, чтобы и x, и y имели тип std::initializer_litsПоследний вариант нравится мне меньше всего (мало кому в реальной жизни заводить локальные переменные типа std::initializer_list), но в стандарт С++11 попал именно он. Постепенно стало выясняться, что это вызывает проблемы у программистов (кто бы мог подумать), поэтому в стандарт добавили патч http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2014/n3922.html, который реализует поведение №2… только в случае copy-list-initialization (auto x = {5}), а в случае direct-list-initialization (auto x{5}) оставляет все по-старому.

Да, я опечатался и перепутал в последнем предложении copy и direct, сейчас исправлю.

Кстати, обратите внимание, что N3922 — это defect report, и применяется не только в C++17, но и к предыдущим стандартам задним числом, что приводит к интересным результатам при апгрейде компилятора...

С классами-агрегатами вообще обидно вышло. Думаешь: "Ура, наконец-то я могу инициализировать non-POD структуры фигурными скобками, даёшь!" А потом выходит, что шаг вправо. шаг влево — нельзя. Например, aggreate initializtion незьзя использвать, если есть конутруктор или значения по умолчанию для полей (т.н. default member initializers), т.е. нельзя сказать


struct A {
    int x = 0;
};
A obj{1};

а если отказаться от значений по умолчанию, кто-то может создать структуру в виде A obj; и получить неинициализированные поля.
К счастью, в 14'м стандарте одумались и member initializers разрешили (но до него надо еще дожить, точнее доапгрейдиться).


В своих лекциях про C++11 Scott Meyers говорил, что когда возникает новая хорошая идея (инициализировать массивы списком элементов через {}), комитет сразу думает: "ага, а давайте добавим это везде!" (разрешим в фигурных скобках писать аргументы конструктора). В итоге получается не локальная фишечка типа описанного вами std::of, а такие вот монстры, которые пытаются решить все потенциально возможные задачи.

https://godbolt.org/g/MItwzR


gcc 4.7.1 вышла в 12 году) clang 3.1 примерно в это же время. Так что пора уже юзать современные компиляторы)
(про msvc не понятно, но последняя версия точно собирает)

Иногда проекты не позволяют(

C MSVC понятно, что в 2015-й студии и ранее не работает, т.е. работает только в 2017-й. Коммерческая разработка не может позволить себе обновлять компиляторы каждые пару лет, увы: есть нестабильность только что вышедших версий, немалые трудозатраты на апгрейд и отлов вызванных им багов, стоимость лицензий (если речь о платных средах).

То, о чем вы говорите, появилось в с++11 и исчезло в с++14. что до
struct A {
    int x = 0;
};

То это расширение gcc, появившееся еще до с++11
Если бы не разрешили как аргументы конструктора, то нельзя было инициализировать в декларации класса члены вызовом конструктора, а это очень нужная вещь, про которую как-то совсем забыто в статье.
class C {
std::atomic_int refCount{1};
};
который реализует поведение №2… только в случае direct-list-initialization (auto x{5}), а в случае copy-list-initialization (auto x = {5}) оставляет все по-старому.

Я не могу это комментирвать. По-моему, это один из очень редких случаев, когда здравый смысл временно покинул авторов языка.

сколько людей, столько и мнений. Когда я изучал с++11/с++14, мне новый вариант показался до боли логичным. Смотрите: в auto x = {1}; тип правой части ({1}) — initializer_list, а тип левой части такой же, как и тип правой. А выражение auto x {1}; — «создать переменную выведенного типа, проинициализировав её значением 1».

Если один конструктор не подходит, мы берем второй, правильно?

Просто надо знать, что конструктор от initializer_list жадный по части перегрузок.

Тогда я, пожалуй, прокомментирую, почему мне это не нравится :)


  • Это не очень консистентно: auto x = 5; и auto x(5); значат одно и то же, auto x = {5};и auto x{5}; — разное, а что делать с auto x({5}); — вообще не очень ясно
  • Такая запись прививает ложное чувство, что {1} — это выражение с типом std::initializer_list<int>, что не так. Это вообще не выражение, у него нет типа, а такое его использование с auto-переменными — отдельно прописанное исключение (второе такое же — это range-based for). Мне кажется, исключений в C++ и так достаточно :)
  • Практически никогда не требуется создавать локальную переменную типа std::initializer_list, а язык поощрает такое поведение
Скажем так: предлагаемые вами альтернативы хуже и с ними будут другие проблемы
Практически никогда не требуется создавать локальную переменную типа std::initializer_list, а язык поощрает такое поведение

Иногда это именно то, что требуется:
auto values = {MyEnum1, myEnum15, MyEnum23};
for (auto val : values) { //...


Теперь мне любопытно что вас не устроило в range-based for

Интересно, а почему нельзя было сделать всё однозначно, чтобы при передаче параметров в фигурных скобках вызывались только конструкторы, принимающие исключительно std::initializer_list? А все остальные конструкторы вызывались с круглыми скобками. Большая часть спорных случаев бы сразу пропала.

Можно пойти еще дальше и вообще не вводить list-initialization в таком виде, а конструкторы вызывать всегда скобочками, например std::vector<int> v({1, 2, 3});.


Но тогда бы не были достигнуты другие цели:


  • Универсальность (иначе инициализация C-структур, масисвов и примитивных типов через фигурные скобки осталась бы странным частным случаем)
  • Защита от most vexing parse
  • Защита от narrowing conversions

Стоили ли эти цели того, что получилось? Насколько я понимаю, в сообществе нет консенсуса по этому поводу. Мое мнение я написал в статье — сама по себе list-initialization — ок, а вот правила про std::initializer_list получились не очень.


Хотя и Вашим бы предложением ничего страшного не случилось бы, как мне кажется.

Посмотрите мой комент выше по поводу инициализации членов класса конструктором. Фигурные скобки призваны в том числе устранить неднозначность синтаксиса (ambiguity), в декларации класса инициализация с круглыми скобками парсится как декларация метода. Почему-то никто не знает об этом важном моменте.

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


После такого начинаются нравиться языки, где объявлению переменных и/или функций предшествуют ключевые слова типа var и function.

Вряд ли кто-то всерьёз утверждает, что C++ — идеальный язык. Но лично меня он вполне устраивает, если писать вдумчиво и аккуратно (auto x = {42} — ну никто ведь не будет такое в здравом уме писать), не задаваясь при этом целью подсчитать кто что где и сколько мог себе отстрелить при его использовании. Нововведение с фигурными скобками лично для меня безусловно к лучшему, при всех его минусах, в основном, кстати, из-за данной возможности вызывать конструкторы членов в декларации класса, часто этим пользуюсь.
Нововведение с фигурными скобками лично для меня безусловно к лучшему, при всех его минусах, в основном, кстати, из-за данной возможности вызывать конструкторы членов в декларации класса, часто этим пользуюсь.

Ну да, просто синтаксический сахар, но выглядит все равно как костыль, а не нормальное решение.

нельзя. int x(); — объявление функции, а не создание x default-конструктором.
Если я хочу вызвать функцию со значением, созданным по умолчанию, я не могу использовать синтаксис Foo(());
return (); — это что за покемон?
Плюс к тому, большая часть спорных случаев бы пропала вместе с возможностью не писать имя класса полностью — то, для чего весь этот сыр-бор и затевался. Что лучше: сделать 30% задачи идеально или 100%, но удовлетворительно?
Правила перегрузки со списками инициализации вообще безумные. То, что один элемент списка приводит к поиску других конструкторов, регулярно ломает код. Например:
struct InitMap {
  using Map = map<string, string>;
  InitMap(Map m) {}
};
InitMap m({{"k", "v"}});

Компилятору непонятно, то ли конструктор копирования звать, то ли конструктор от map. Добавляем «пустой» элемент:
InitMap m({{"k", "v"}, {});

И всё работает. Самая магия, что вариант
InitMap m({{string("k"), "v"}});

Работает, а приведение обоих элементов нет:
InitMap m({{string("k"), string("v")}});

Это всё объяснимо и после поллитра, а то и больше, даже понятно. Но лучше бы не усложняли инициализацию. Задача сделать универсальную инициализацию на все случаи жизни так и не решена. Куча мест, где в шаблонах нельзя бездумно написать {}, иногда ещё и две версии приходится делать.
По-моему, это один из очень редких случаев, когда здравый смысл временно покинул авторов языка.


Честно, я не припомню ни одного случая, когда здравый смысл бы их покинул.

Попробуйте скомпилировать вот этот абстрактный пример (прошу обратить внимание на конструкторы):
struct Foo {
    Foo() {
        // unpredictable logic
    }
    Foo(std::initializer_list<int>) {
        // more unpredictable logic
    }
    explicit Foo(const Foo&) {
        // even more unpredictable logic
    }
};

void main() {
    auto foo(Foo());
    std::cout << typeid(foo).name() << std::endl;
}

Подсказка: он не скомпилируется. Почему? Да-да, тот самый most vexing parse, поэтому мы получим совсем не то, что хотели. Решение? Давайте подумаем.

Написать так?
auto foo = Foo()
Нельзя, потому что конструктор копирования explicit.

Написать так?
auto foo(Foo{})
Нельзя, потому что мы хотим вызвать именно конструктор по умолчанию.

Очевидно! Написать так:
auto foo{Foo()}


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

PS:
Пример, конечно, совершенно бесполезный, но подобная ситуация не является чем-то невероятным.

Интересно, а в каких случаях конструктор копирования приходится объявлять как explicit?

Например, когда у него есть серьезные side-эффекты. Ну или просто хочется запретить бездумное копирование.

Тогда разумнее просто сделать Foo(const Foo&) = delete. А обдуманное копирование всегда можно реализовать какой-нибудь функцией.


В этом случае copy-initialization ломаться не будет, а благодаря copy elision вариант auto foo = Foo() будет эквивалентен вызову конструктора по умолчанию.

И то правда, безусловно. Но иногда в мире случаются удивительные вещи.
Кстати, возможно, я Вас неправильно понял, но copy initialization не будет работать, если конструктор deleted. И неважно, возможно там copy elision или нет.

Но если есть move-конструктор (в т.ч. move-конструктор по умолчанию), то будет работать.

А, теперь все встало на свои места.

В C++14 — да, нужен move-constructor, причем его нужно явно написать (например, = default). В С++17, к счастью, это требование убрали, и все будет работать.

Ммм, не уверен. Даже открыл драфт N4296, но там требования на implicit move constructor не изменились — он все так же не объявляется, если есть user-declared copy constructor (explicit Foo(const Foo&) в данном случае).

Разница между C++14 и C++17 заключается именно в copy elision. Стандарт C++17 обязывает игнорировать конструкторы при copy-initialization, поэтому код будет работать всегда, если просто написать auto foo = Foo();.


В C++14 же для copy-initialization требуется подходящий конструктор, даже если компилятор его и выкинет.

Да, будет работать, теперь вижу. К сожалению, из всех нововведений я в курсе лишь о тех, что случайно бросились в глаза на том же cppreference, например, потому спасибо за разъяснения.

Да, я за то, чтобы вообще запретить выводить std::initializer_list для auto переменных, это и пытался изложить в статье.


Наверняка текущий странный вариант не так просто приняли, но причин я пока не понял. Все, что есть в n3922 — это параграф, который не объясняет проблем:


There was also support (though less strong) in EWG for an alternative proposal in which auto deduction does not distinguish between direct list-initialization and copy list-initialization, and which allows auto deduction from a braced-init-list only in the case where the braced-init-list consists of a single item. Possible wording was for that alternative was included in N3681 by Voutilainen.

В N3922 есть следующая отсылка:


For background information see N3681, "Auto and braced-init-lists", by Voutilainen, and N3912, "Auto and braced-init-lists, continued", also by Voutilainen.

При этом в N3912, хоть и без лишних подробностей, объясняется почему и как было принято именно такое решение. Основная причина, я так понимаю, в том, чтобы не сломать range-based for в случае, когда в качестве range expression используется braced-init-list.

К слову, там же объясняется причина появления defect report, который изменил поведение auto в случае direct-initialization (что мы тут и обсуждаем, собственно).

Как раз direct-initialization из N3912 я понимаю и поддерживаю.


А про range-based for — мне кажется, что добавить особый случай для braced-init-list именно в него было бы лучше, чем то, что получилось. Ну да это дело вкуса, видимо.

потому что int нельзя проинициализировать с помощью {{}}

почему же нельзя? можно:
int x{{}};

Не люблю инициализаторы в стиле с++, потому, что не видно, что куда идет. Т.е., использовать можно для простых структур, а если нужно инициализировать 3 и больше полей - то уже неясно, что куда присваивается. В этом плане лучше продуман стиль С99, синтаксис похож на раст: `Foo {.a=20, .b=30, .c=40,....}`

Sign up to leave a comment.

Articles