LINUX.ORG.RU

Инициализация полей класса

 ,


0

1

Читаю книгу Beautiful C++. Там рассматривается вопрос о правильной инициализации полей класса и в качестве начального примера используется следующий код:

class piano
{
public:
  piano();
private:
  int number_of_keys;
  bool mechanical;
  std::string manufacturer;
};

piano::piano()
{
  number_of_keys = 88;
  mechanical = true;
  manufacturer = "Yamaha";
}

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

piano::piano()
  : number_of_keys(88)
  , mechanical(true)
  , manufacturer("Yamaha")
{}

Мне более привычен немного другой стиль написания:

piano::piano() :
    number_of_keys(88),
    mechanical(true),
    manufacturer("Yamaha")
{}

А вот ещё нашёл документ, описывающий гугловский стиль написания кода на C++ https://google.github.io/styleguide/cppguide.html#Constructor_Initializer_Lists и в нём этот код выглядел бы вот так:

piano::piano()
    : number_of_keys(88),
      mechanical(true),
      manufacturer("Yamaha") {
}

Какой стиль инициализации в конструкторе вы предпочитаете?

P.S. на этом разбор инициализации в книге не заканчивается, но меня заинтересовал стиль написания кода.



Последнее исправление: zg (всего исправлений: 1)

Это старый стиль, сейчас можно писать так:

piano::piano()
    : number_of_keys{88},
      mechanical{true},
      manufacturer{"Yamaha"} {
}

но если они дефолтные, то можно указывать прямо в классе:

class piano
{
public:
	piano();
private:
	int number_of_keys {88};
	bool mechanical {true};
	std::string manufacturer {"Yamaha"};
};
soomrack ★★★★
()
Ответ на: комментарий от soomrack

Это старый стиль, сейчас можно писать так:

piano::piano()
    : number_of_keys{88},
      mechanical{true},
      manufacturer{"Yamaha"} {
}

Видимо ты об этом: https://www.youtube.com/watch?v=Dg77_dp8jgE Но разве это имеет какое-то значение в блоке инициализации?

zg
() автор топика
Ответ на: комментарий от zg

Лень смотреть, но да, {} – для инициализации, а () для вызова конструктора.

В твоем примере, manufacturer{"Yamaha"} будет сразу создан string со значением Yamaha, а manufacturer("Yamaha") вызовет конструктор std::string(char *)

soomrack ★★★★
()
Ответ на: комментарий от Dr64h

Ну вот в Java все пишут в более-менее одном стиле. Иногда бывают местные заскоки, как например глупое требование везде и всюду объявлять аргументы методов final, но с этим можно жить.

zg
() автор топика
Ответ на: комментарий от soomrack

В твоем примере, manufacturer{«Yamaha»} будет сразу создан string со значением Yamaha, а manufacturer(«Yamaha») вызовет конструктор std::string(char *)

Что значит «сразу»? Разве не будет вызван один из конструкторов std::string?

zg
() автор топика
Ответ на: комментарий от zg

Что значит «сразу»?

man initializer_list

https://en.cppreference.com/w/cpp/utility/initializer_list

если коротко, то {} приведет к созданию прокси-объекта, данные которого быстро перенесутся в основной объект. Это может быть сильно быстрее и удобнее, чем прямой вызов конструктора, тем более, с конвертацией из другого типа….

Пример различия:

Vec* v1 = new std::vector(10); // vector из 10 элементов заполненный 10 нулями
Vec* v2 = new std::vector{10}; // vector из одного элемента в котором значение 10

PS: конструктор, само собой, для std::string все равно вызовется, но аргумент будет initializer_list<>

soomrack ★★★★
()
Последнее исправление: soomrack (всего исправлений: 1)
Ответ на: комментарий от zg

Мои рекомендации по стилю Вам скорее навредят, у нас специфическая область и свои стандарты в т.ч. касательно стиля.

Стиль определяет работодатель.

AntonI ★★★★★
()
Последнее исправление: AntonI (всего исправлений: 1)

Если мы о форматировании говорим, то лично я предпочитаю первый вариант из соображений минимизации diff’ов при добавлении и удалении полей класса. Если мы о NSDMI vs классическая инициализация то зависит: если конструктор единственный - то всё проинициализирую по старинке в одном месте чтобы читать было удобней, если конструкторов много - то начинаются варианты и в основном руководствуюсь common sense.

А вообще я бы рекомендовал следовать одному главному правилу: «write what you know, know what you are writing», и всё у вас получится :)

bugfixer ★★★★★
()
Ответ на: комментарий от Beewek

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

Да, с двоеточием там лажа. Если оно есть, блок инициализации обязательно должен быть после. По-моему это немного странное требование C++, ну да ладно. Как насчёт такого стиля?

piano::piano()
    :
    number_of_keys(88),
    mechanical(true),
    manufacturer("Yamaha")
{}
zg
() автор топика

Есть clang-format, можно отдать ему работу по форматированию кода.

Для vim использую такое, если в папке из которой запускается vim лежит файл .clang-format, то при сохранении файлов с форматом .c, .cc, .h применяется автоформатирование.

function! ClangFormat()
  if filereadable(".clang-format")
    let l=line(".")
    execute 'silent %!clang-format'
    execute l
  endif
endfunction
autocmd BufWritePre *.c,*.cc,*.h call ClangFormat()

.clang-format генерируется так

clang-format -style=llvm -dump-config > .clang-format

Изобретать свой стиль, когда есть уже готовые не вижу смысла.

MOPKOBKA ★★★★
()
Последнее исправление: MOPKOBKA (всего исправлений: 2)
Ответ на: комментарий от AntonI

Стиль определяет работодатель.

В больших долгоиграющих проектах - скорее группа людей которым это право (и обязанность) делегированы.

bugfixer ★★★★★
()
Последнее исправление: bugfixer (всего исправлений: 1)
Ответ на: комментарий от bugfixer

Главное что не аннунаки с аннунашками, а то заставят мегалитически оформлять код - заманаешься зубилом стучать;-)

Каменоломня древних ануннаков?

AntonI ★★★★★
()
Последнее исправление: AntonI (всего исправлений: 1)
Ответ на: комментарий от soomrack

man initializer_list

https://en.cppreference.com/w/cpp/utility/initializer_list

Причём тут initializer_list? В случае manufacturer{ "Yamaha" } в Visual C++ вызывается это:

    _CONSTEXPR20 basic_string(_In_z_ const _Elem* const _Ptr) : _Mypair(_Zero_then_variadic_args_t{}) {
        _Construct<_Construct_strategy::_From_ptr>(_Ptr, _Convert_size<size_type>(_Traits::length(_Ptr)));
    }

В случае же manufacturer("Yamaha") там вызывается ровто то же самое.

PS: конструктор, само собой, для std::string все равно вызовется, но аргумент будет initializer_list<>

В обоих случаях я наблюдаю в дебагере аргумент типа const char * const.

zg
() автор топика
Последнее исправление: zg (всего исправлений: 1)

Всё равно как. Хоть совсем безобразно, главное, чтобы единообразно. Поэтому автоформат, настроенный на стиль проекта. Если проект новый, выбирай любой, кроме самого, на твой взгляд, ублюдышного.

witaway
()
Последнее исправление: witaway (всего исправлений: 1)
Ответ на: комментарий от witaway

Поэтому автоформат, настроенный на стиль проекта

Удачи в разборках с остальной командой если не дай бог вы где-то в настройках своего авто-форматера промахнулись, ага :)

bugfixer ★★★★★
()
Ответ на: комментарий от bugfixer

Ага, ошибся.

Но мой подход можно сформулировать просто:

если я хочу чтобы было проинициализировано значением, которое я указал, то я использую {},

если же я указывают параметры для создания объекта (не его значение!), то я использую (),

так код выглядит более понятным для меня.

По поводу оформления:

стиль задается принятым стилем в проекте,

если есть дефолтные значения, то их лучше указать в объявлении класса и не нести в конструктор, тогда мы будем уверены, что они всегда будут так инициализированы, вне зависимости от вызванного конструктора,

в остальных случаях я предпочитаю писать их в одну строку:

Piano::Piano(const int number_of_keys, const bool mechanical) 
    : number_of_keys{88}, mechanical{true}
{}

если параметров много, тогда столбиком друг под другом, но это редко бывает,

скобку функции ставлю на след. строке (стиль Страуструпа).

soomrack ★★★★
()
Ответ на: комментарий от bugfixer

А общий для всех конфиг в корне на что? На крайний случай, если уж обкакался со стилями — поправь настройки, переформатируй свой код и больше так не промахивайся. Дело 10 минут.

С автоформаттером промахнёшься один раз и то вряд ли — форматируя ручками будешь промахиваться регулярно и почти наверняка.

witaway
()

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

В данном конкретном примере разницы не будет, т.к. типы простые и инициализация значениями по-умолчанию происходит естественным образом при выделении памяти. Вот если это будут классы, тогда разница будет существенная.

no-such-file ★★★★★
()
Ответ на: комментарий от witaway

А общий для всех конфиг в корне на что?

Действительно. Ну, побегайте, заставьте пару тысяч людей использовать именно его. А уж как весело будет изменения в него вносить, ууу :)

На крайний случай, если уж обкакался со стилями

Это не вопрос «if», это вопрос «when».

переформатируй свой код и больше так не промахивайся. Дело 10 минут

No comments.

bugfixer ★★★★★
()
Ответ на: комментарий от no-such-file

Автор книжки болван в таком случае

Да не, всё правильно он написал. Просто мне не понятно почему ТС упор именно на форматировании делает - мне кажется основная мысль там совсем другая (initialization vs assignment).

bugfixer ★★★★★
()
Ответ на: комментарий от zg

Как насчёт такого стиля?

Это на мой взгляд хуже, чем когда двоеточие на одной строчке с piano(). Но вообще я думаю, что это не такой уж важный вопрос. Важно, чтобы каждый инициализатор был на отдельной строчке, это даёт более читаемые диффы. А остальное - по вкусу.

Beewek ★★
()
Ответ на: комментарий от bugfixer

Просто мне не понятно почему ТС упор именно на форматировании делает - мне кажется основная мысль там совсем другая (initialization vs assignment).

Видимо с initialization vs assignment всё понятно - по возможности предпочитать initialization. А вот в форматировании есть варианты.

Beewek ★★
()
Ответ на: комментарий от soomrack

если я хочу чтобы было проинициализировано значением, которое я указал, то я использую {}, если же я указывают параметры для создания объекта (не его значение!), то я использую ()

Логика есть, но из-за постоянных «warning: narrowing conversion» по поводу и без делает инициализацию через {} болезненой. С 17 (вроде) стандарта можно агрегатную инициализацию в круглых скобках. По сути использую круглые по максимуму, редко {}.

kvpfs_2
()
Ответ на: комментарий от bugfixer

Не исключаю, возможно, по своей малоопытности я не замечаю слона в комнате — но где здесь, собственно, проблема?

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

Заставить пару тысяч людей щепетильно своими ручками писать в одном едином стиле явно всё ещё сложнее, чем навязать им использование одного единого конфига.

Особенно с учётом того, что обычно тулзы сами эти конфиг подхватывают. Особенно с учётом того, что гит-хуки всё ещё существуют.

Это не вопрос «if», это вопрос «when».

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

No comments.

Of course, дело 10 минут только если на проекте есть хотя бы минимальный процесс ревью… В ином случае, согласен. Обнаружили через полгода, прямо здесь и сейчас оперативно за собой уже не почистить. Беда.

Если я неправ, поделитесь опытом. 🤷‍♂️

witaway
()
Последнее исправление: witaway (всего исправлений: 1)
Ответ на: комментарий от bugfixer

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

MOPKOBKA ★★★★
()
Ответ на: комментарий от soomrack

Боль скорее вытекает из-за беззнаковости размеров в std, я данный подход не разделяю (не лишь я, а в тех же core guidelines тоже), беззнаковым место в масках всяких, флагах. В итоге простейший случай «Type t{vec.size()}» вызывает боль, либо костылить static_cast, либо брать круглые скобки.

Т.е. начинаются постоянные отступления от правила, которое вы озвучили (я в одно время тоже пытался ему следовать). Ну и нет последовательности - int = unsigned - ок, int (uint) - ok; int {uint} = warning. В общем подумал я однажды и вовсе отказался по максимуму от {}, лишь головная боль от них. Зря эту дрянь про narrowing conversion впихнули в стандарт, могло бы быть красиво.

kvpfs_2
()
Ответ на: комментарий от kvpfs_2

Ну в целом да, есть проблемы, но для многих вещей есть принципиальная разница в {} и (), как пример с

std::vector(10);
std::vector{10};

или

std::string(40, '*');  // ***...40 раз...***
std::string{40, '*'};  // (*

и вроде как сейчас хотят вводить знаковый size…

PS: narrowing conversion warning можно, наверное, и отключить…

soomrack ★★★★
()
Последнее исправление: soomrack (всего исправлений: 1)
Ответ на: комментарий от kvpfs_2

беззнаковым место в масках всяких, флагах.

И в тех случаях, где может быть переполнение, которые фиксируется постфактум, или которое не нужно фиксировать вовсе.

soomrack ★★★★
()
Последнее исправление: soomrack (всего исправлений: 1)
Ответ на: комментарий от soomrack

narrowing conversion warning можно, наверное, и отключить…

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

zg
() автор топика
Ответ на: комментарий от soomrack

И в тех случаях, где может быть переполнение, которые фиксируется постфактум, или которое не нужно фиксировать вовсе.

Ну всё это спец кейсы, на которые смотрят под лупой. Гораздо хуже пропустить вот такой микс: «a.get_uint() - b.get_int() + get_uint()». Не место беззнаковым в рутинных, тривиальных кейсах.

В общем идея может и хорошей была, но реальность была с оврагами. Например, вот что в ГЦЦ говорили перестав ронять компиляцию с ошибкой про новую фичу:

The standard only requires that "a conforming implementation shall issue at least one diagnostic message" so compiling the program with a warning is allowed.  As Andrew said, -Werror=narrowing allows you to make it an error if you want.

G++ 4.6 gave an error but it was changed to a warning intentionally for 4.7 because many people (myself included) found that narrowing conversions where one of the most commonly encountered problems when trying to compile large C++03 codebases as C++11.  Previously well-formed code such as char c[] = { i, 0 }; (where i will only ever be within the range of char) caused errors and had to be changed to char c[] = { (char)i, 0 }
kvpfs_2
()
Последнее исправление: kvpfs_2 (всего исправлений: 2)