Об инициализации структур

Не так давно я зацепился глазом за “фигурно-скобоную” инициализацию структуры в одной из тем. Была создана структура

struct commandLine {
  uint16_t timeFromStart;
  uint8_t deviceName;
  boolean onDevice;
  uint16_t timeDeviceOn;
};

которая затем инициировалась (в составе массива вот так)

  commandLine Formovka[] = {
    (commandLine){ 17, 2, 1, 170 },
    (commandLine){ 8, 3, 1, 88 },
    (commandLine){ 5, 2, 1, 55 },
    (commandLine){ 10, 3, 1, 100 },
    (commandLine){ 21, 2, 1, 210 },
    (commandLine) { 13, 3, 1, 130 }
  };

Я тогда ругнулся, что мол это “говнокод” и предложил её заменить не чем иным, как другим говнокодом. Чуть получше, но тоже …

commandLine formovka[] = {
	{ .timeFromStart = 17, .deviceName = 2, .onDevice = true, .timeDeviceOn = 170, .name = "Dedka" },
	{ .timeFromStart = 8,  .deviceName = 3, .onDevice = true, .timeDeviceOn = 88,  .name = "Babka" },
	{ .timeFromStart = 5,  .deviceName = 2, .onDevice = true, .timeDeviceOn = 55,  .name = "Vnuchka" },
	{ .timeFromStart = 10, .deviceName = 3, .onDevice = true, .timeDeviceOn = 100, .name = "Zhuchka" },
	{ .timeFromStart = 21, .deviceName = 2, .onDevice = true, .timeDeviceOn = 210, .name = "Koshka" },
	{ .timeFromStart = 13, .deviceName = 3, .onDevice = true, .timeDeviceOn = 130, .name = "Myshka" }
};

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

Ну, вот давайте начнём. Начнём в первого пример (до “моего” примера мы ещё доберёмся и ему тоже достанется.

Вариант с безымянной фигурно-скобочной инициализацией

Итак, первый пример, что с ним не так?

Для начала заметим, что синтаксически такую инициализацию можно выразить по разному. Можно, например, вообще убрать преобразование к типу или поставить не не такое (имя типа в скобках) в т.н. функциональной нотации. В примере ниже используются все три варианта (строки №№ 28-30) и всё, как видите, работает на ура.

Пример фигурно-скобочно инициализации в трёх вариантах
//
//	Простая структура с методом печати в 
//	поток (например, Serial, SoftSerial, LCD и т.п.)
//
struct SBox {
	int width;	//	ширина
	int length;	//	длина
	int height;	//	высота
	int weight;	//	вес

	void print(Print& p) const {
		p.print("{ width: ");
		p.print(width);
		p.print("; length: ");
		p.print(length);
		p.print("; height: ");
		p.print(height);
		p.print("; weight: ");
		p.print(weight);
		p.println(" }");
	}
};

//
// Массив структур, каждый элемент инициализируем чуть по-разному
//
SBox payLoad[] {
	{ 40, 200, 32, 43 },
	(SBox) { 38, 120, 40, 26 },
	SBox({ 60, 150, 48, 51 })
};
//	Количество элементов в массиве
static constexpr uint8_t totalBoxes = sizeof(payLoad) / sizeof(payLoad[0]);

//
//	Просто печатаем массив, чтобы посмотреть, 
//	как отработала инициализация 
//
void setup() {
	Serial.begin(9600);
	for(uint8_t i = 0; i < totalBoxes; ++i) {
		Serial.print("payLoad[");
		Serial.print(i);
		Serial.print("]=");
		payLoad[i].print(Serial);
	}
}

//	Здесь ничего не делаем
void loop() { delay(100500); }
Результат работы примера
payLoad[0]={ width: 40; length: 200; height: 32; weight: 43 }
payLoad[1]={ width: 38; length: 120; height: 40; weight: 26 }
payLoad[2]={ width: 60; length: 150; height: 48; weight: 51 }

Чтобы закрыть эту тему и больше не к ней не возвращаться, скажу, что никакой разницы между этими тремя синтаксисами инициализации нет, они идентичны. Первый (в строке №28) чуть хуже остальных тем, что там не указан тип. Компилятор-то разберётся, а вот для читателя-человека тип лучше указывать. Но функционально они идентично и впредь я не буду их разделять, а буду говорить о граблях фигурно-скобочной инициализации вообще – это всех трёх синтаксисов касается.

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

Грабли №1

Выделим грабли №1: структура, даже системная, всеми и много используемая и очень старая, может иметь разный порядок следования членов. И если типы у членов одинаковы (или совместимые), то компилятор ничего не заметит, просто присвоит значения не тем членам, каким нужно и всё пойдёт наперекосяк. Если типы одинаковые, то бороться с этим нельзя никак (компилятору-то всё равно какому полю структуры что присваивать. Если это не системная структура (как было у меня по ссылке), а Ваша, то хорошо бы, когда Вы её меняете, заодно менять и все инициализации. Но, положа руку на сердце, Вы точно сможете вспомнить все такие места в проекте из 150 файлов, который Вы разрабатываете уже год? Вот и я так же :frowning:

Попробуйте сами в моём примере выше поменять местами какие-нибудь элементы структуры, а инициализацию не трогайте и убедитесь, что компиляция проскочит “молча”, а числа будут присвоены не тем элементам :frowning:

Грабли №2

На этот раз, мы просто добавим новый элемент в структуру. Для начала в самый конец (строка №10):

Пример с добавлением нового члена структуры в конец
//
//	Простая структура с методом печати в 
//	поток (например, Serial, SoftSerial, LCD и т.п.)
//
struct SBox {
	int width;	//	ширина
	int length;	//	длина
	int height;	//	высота
	int weight;	//	вес
	int color;

	void print(Print& p) const {
		p.print("{ width: ");
		p.print(width);
		p.print("; length: ");
		p.print(length);
		p.print("; height: ");
		p.print(height);
		p.print("; weight: ");
		p.print(weight);
		p.println(" }");
	}
};

//
// Массив структур, каждый элемент инициализируем чуть по-разному
//
SBox payLoad[] {
	{ 40, 200, 32, 43 },
	(SBox) { 38, 120, 40, 26 },
	SBox({ 60, 150, 48, 51 })
};
//	Количество элементов в массиве
static constexpr uint8_t totalBoxes = sizeof(payLoad) / sizeof(payLoad[0]);

//
//	Просто печатаем массив, чтобы посмотреть, 
//	как отработала инициализация 
//
void setup() {
	Serial.begin(9600);
	for(uint8_t i = 0; i < totalBoxes; ++i) {
		Serial.print("payLoad[");
		Serial.print(i);
		Serial.print("]=");
		payLoad[i].print(Serial);
	}
}

//	Здесь ничего не делаем
void loop() { delay(100500); }
Сообщения компилятора
D:\GoogleD\Soft\Uroki\StructInit\SturctInit1\SturctInit1.ino:30:27: warning: missing initializer for member 'SBox::color' [-Wmissing-field-initializers]
  (SBox) { 38, 120, 40, 26 },
                           ^
D:\GoogleD\Soft\Uroki\StructInit\SturctInit1\SturctInit1.ino:31:26: warning: missing initializer for member 'SBox::color' [-Wmissing-field-initializers]
  SBox({ 60, 150, 48, 51 })
                          ^
D:\GoogleD\Soft\Uroki\StructInit\SturctInit1\SturctInit1.ino:32:1: warning: missing initializer for member 'SBox::color' [-Wmissing-field-initializers]
 };
 ^
Скетч использует 2172 байт (6%) памяти устройства. Всего доступно 32256 байт.
Глобальные переменные используют 276 байт (13%) динамической памяти, оставляя 1772 байт для локальных переменных. Максимум: 2048 байт.
Результат работы примера
payLoad[0]={ width: 40; length: 200; height: 32; weight: 43 }
payLoad[1]={ width: 38; length: 120; height: 40; weight: 26 }
payLoad[2]={ width: 60; length: 150; height: 48; weight: 51 }

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

Отработало, кстати, всё штатно - все числа на месте. Другое дело, если мы вставим новый член не в конец, а в начало или в середину. Давайте попробуем (строка №6):

Пример с добавлением нового члена структуры в начало
//
//	Простая структура с методом печати в 
//	поток (например, Serial, SoftSerial, LCD и т.п.)
//
struct SBox {
	int color;
	int width;	//	ширина
	int length;	//	длина
	int height;	//	высота
	int weight;	//	вес

	void print(Print& p) const {
		p.print("{ width: ");
		p.print(width);
		p.print("; length: ");
		p.print(length);
		p.print("; height: ");
		p.print(height);
		p.print("; weight: ");
		p.print(weight);
		p.println(" }");
	}
};

//
// Массив структур, каждый элемент инициализируем чуть по-разному
//
SBox payLoad[] {
	{ 40, 200, 32, 43 },
	(SBox) { 38, 120, 40, 26 },
	SBox({ 60, 150, 48, 51 })
};
//	Количество элементов в массиве
static constexpr uint8_t totalBoxes = sizeof(payLoad) / sizeof(payLoad[0]);

//
//	Просто печатаем массив, чтобы посмотреть, 
//	как отработала инициализация 
//
void setup() {
	Serial.begin(9600);
	for(uint8_t i = 0; i < totalBoxes; ++i) {
		Serial.print("payLoad[");
		Serial.print(i);
		Serial.print("]=");
		payLoad[i].print(Serial);
	}
}

//	Здесь ничего не делаем
void loop() { delay(100500); }
Сообщения компилятора
D:\GoogleD\Soft\Uroki\StructInit\SturctInit1\SturctInit1.ino:30:27: warning: missing initializer for member 'SBox::weight' [-Wmissing-field-initializers]
  (SBox) { 38, 120, 40, 26 },
                           ^
D:\GoogleD\Soft\Uroki\StructInit\SturctInit1\SturctInit1.ino:31:26: warning: missing initializer for member 'SBox::weight' [-Wmissing-field-initializers]
  SBox({ 60, 150, 48, 51 })
                          ^
D:\GoogleD\Soft\Uroki\StructInit\SturctInit1\SturctInit1.ino:32:1: warning: missing initializer for member 'SBox::weight' [-Wmissing-field-initializers]
 };
 ^
Скетч использует 2172 байт (6%) памяти устройства. Всего доступно 32256 байт.
Глобальные переменные используют 276 байт (13%) динамической памяти, оставляя 1772 байт для локальных переменных. Максимум: 2048 байт.
Результат работы примера
payLoad[0]={ width: 200; length: 32; height: 43; weight: 0 }
payLoad[1]={ width: 120; length: 40; height: 26; weight: 0 }
payLoad[2]={ width: 150; length: 48; height: 51; weight: 0 }

Как видите, здесь всё гораздо хуже. Компилятор предупредил (как и раньше) и программа неправильно инициализировала структуру (первое число ушло в инициализацию поля color, и всё сдвинулось). Беда!

Грабли №3

Это особый вид граблей, которые подстерегаю наиболее “продвинутых парней” (не по Пелевину, а в “хорошем смысле”).

Итак, давайте вообразим себя продвинутыми и скажем, а нафига нам в нашей структуре такой лоховской метод print, который требует от нас в писать в коде уродливую конструкцию типа payLoad[i].print(Serial); (строка №46 в последнем примере). Давайте сделаем нашу структуру дружественной к потоковому выводу, чтобы мы могли писать как нормальные люди: Serial.println(payLoad[i]);. ООП тут нас или где? Так давайте и писать по ООП’ному!

Как это делается я уже объяснял, повторяться не буду. Сделаем это:

и тут же налетим на грабли! Структуру, у которой есть нетривиальный конструктор нельзя инициализировать фигурно-скобоxной инициализацией :frowning:

Пример с потоковой печатью
//
//	Простая структура с методом печати в 
//	поток (например, Serial, SoftSerial, LCD и т.п.)
//
struct SBox : public Printable {
	int color;
	int width;	//	ширина
	int length;	//	длина
	int height;	//	высота
	int weight;	//	вес

	size_t printTo(Print& p) const {
		size_t res = p.print("{ width: ");
		res += p.print(width);
		res += p.print("; length: ");
		res += p.print(length);
		res += p.print("; height: ");
		res += p.print(height);
		res += p.print("; weight: ");
		res += p.print(weight);
		res += p.println(" }");
		return res;
	}
};

//
// Массив структур, каждый элемент инициализируем чуть по-разному
//
SBox payLoad[] {
	{ 40, 200, 32, 43 },
	(SBox) { 38, 120, 40, 26 },
	SBox({ 60, 150, 48, 51 })
};
//	Количество элементов в массиве
static constexpr uint8_t totalBoxes = sizeof(payLoad) / sizeof(payLoad[0]);

//
//	Просто печатаем массив, чтобы посмотреть, 
//	как отработала инициализация 
//
void setup() {
	Serial.begin(9600);
	for(uint8_t i = 0; i < totalBoxes; ++i) {
		Serial.print("payLoad[");
		Serial.print(i);
		Serial.print("]=");
		Serial.println(payLoad[i]);
	}
}

//	Здесь ничего не делаем
void loop() { delay(100500); }
Сообщения компилятора
D:\GoogleD\Soft\Uroki\StructInit\SturctInit2\SturctInit2.ino:31:27: error: no matching function for call to 'SBox::SBox(<brace-enclosed initializer list>)'
  (SBox) { 38, 120, 40, 26 },
                           ^
D:\GoogleD\Soft\Uroki\StructInit\SturctInit2\SturctInit2.ino:5:8: note: candidate: SBox::SBox()
 struct SBox : public Printable {
        ^~~~
D:\GoogleD\Soft\Uroki\StructInit\SturctInit2\SturctInit2.ino:5:8: note:   candidate expects 0 arguments, 4 provided
D:\GoogleD\Soft\Uroki\StructInit\SturctInit2\SturctInit2.ino:5:8: note: candidate: constexpr SBox::SBox(const SBox&)
D:\GoogleD\Soft\Uroki\StructInit\SturctInit2\SturctInit2.ino:5:8: note:   candidate expects 1 argument, 4 provided
D:\GoogleD\Soft\Uroki\StructInit\SturctInit2\SturctInit2.ino:5:8: note: candidate: constexpr SBox::SBox(SBox&&)
D:\GoogleD\Soft\Uroki\StructInit\SturctInit2\SturctInit2.ino:5:8: note:   candidate expects 1 argument, 4 provided
D:\GoogleD\Soft\Uroki\StructInit\SturctInit2\SturctInit2.ino:32:26: error: no matching function for call to 'SBox::SBox(<brace-enclosed initializer list>)'
  SBox({ 60, 150, 48, 51 })
                          ^
D:\GoogleD\Soft\Uroki\StructInit\SturctInit2\SturctInit2.ino:5:8: note: candidate: SBox::SBox()
 struct SBox : public Printable {
        ^~~~
D:\GoogleD\Soft\Uroki\StructInit\SturctInit2\SturctInit2.ino:5:8: note:   candidate expects 0 arguments, 1 provided
D:\GoogleD\Soft\Uroki\StructInit\SturctInit2\SturctInit2.ino:5:8: note: candidate: constexpr SBox::SBox(const SBox&)
D:\GoogleD\Soft\Uroki\StructInit\SturctInit2\SturctInit2.ino:5:8: note:   no known conversion for argument 1 from '<brace-enclosed initializer list>' to 'const SBox&'
D:\GoogleD\Soft\Uroki\StructInit\SturctInit2\SturctInit2.ino:5:8: note: candidate: constexpr SBox::SBox(SBox&&)
D:\GoogleD\Soft\Uroki\StructInit\SturctInit2\SturctInit2.ino:5:8: note:   no known conversion for argument 1 from '<brace-enclosed initializer list>' to 'SBox&&'

exit status 1

Compilation error: no matching function for call to 'SBox::SBox(<brace-enclosed initializer list>)'

Опять же, заметьте, ругается на все три синтаксиса – ну, нет между ними разницы.

Ну, хоть ошибку выдал, а не предупреждение и “неправильную работу” как было раньше. И на том спасибо :frowning:

Вариант с инициализаций по именам полей

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

Грабли всё те же, поэтому я не буду их подробно описывать, просто буду ссылаться на них по номерам. Подробно опишу только новые.

Изначальный код. Возвращаем наш “лоховской” print и делаем инициализацию по именам полей.

Пример с инициализацией по именам полей
//
//	Простая структура с методом печати в 
//	поток (например, Serial, SoftSerial, LCD и т.п.)
//
struct SBox {
	int width;	//	ширина
	int length;	//	длина
	int height;	//	высота
	int weight;	//	вес

	void print(Print& p) const {
		p.print("{ width: ");
		p.print(width);
		p.print("; length: ");
		p.print(length);
		p.print("; height: ");
		p.print(height);
		p.print("; weight: ");
		p.print(weight);
		p.println(" }");
	}
};

//
// Структура проинициализированная по именам
//
SBox payLoad = {
	.width = 40, 
	.length = 200,
	.height = 32,
	.weight = 43
};

//
//	Просто печатаем структуру, чтобы посмотреть, 
//	как отработала инициализация 
//
void setup() {
	Serial.begin(9600);
	payLoad.print(Serial);
}

//	Здесь ничего не делаем
void loop() { delay(100500); }
Результат работы примера
{ width: 40; length: 200; height: 32; weight: 43 }

Вроде всё нормально.

Грабли №1

Меняем местами элементы и пробуем откомпилировать

Пример со сменой мест элементов
//
//	Простая структура с методом печати в 
//	поток (например, Serial, SoftSerial, LCD и т.п.)
//
struct SBox {
	int length;	//	длина
	int height;	//	высота
	int width;	//	ширина
	int weight;	//	вес

	void print(Print& p) const {
		p.print("{ width: ");
		p.print(width);
		p.print("; length: ");
		p.print(length);
		p.print("; height: ");
		p.print(height);
		p.print("; weight: ");
		p.print(weight);
		p.println(" }");
	}
};

//
// Массив структур, каждый элемент инициализируем чуть по-разному
//
SBox payLoad = {
	.width = 40, 
	.length = 200,
	.height = 32,
	.weight = 43
};

//
//	Просто печатаем структуру, чтобы посмотреть, 
//	как отработала инициализация 
//
void setup() {
	Serial.begin(9600);
	payLoad.print(Serial);
}

//	Здесь ничего не делаем
void loop() { delay(100500); }
Сообщения компилятора
D:\GoogleD\Soft\Uroki\StructInit\SturctInit1\SturctInit1.ino:32:1: sorry, unimplemented: non-trivial designated initializers not supported
 };
 ^
D:\GoogleD\Soft\Uroki\StructInit\SturctInit1\SturctInit1.ino:32:1: sorry, unimplemented: non-trivial designated initializers not supported
D:\GoogleD\Soft\Uroki\StructInit\SturctInit1\SturctInit1.ino:32:1: sorry, unimplemented: non-trivial designated initializers not supported

exit status 1

Compilation error: exit status 1```

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

Грабли №2

Добавление нового элемента в структуру приведёт опять же к честной ошибке компиляции (попробуйте проделать это самостоятельно). Ну, а раз это ошибка, а не предупреждение, считаем, что и здесь всё значительно лучше, чем было при “безымянном подходе”.

Грабли №3

Ну, что, опять вставляем потоковую печать и любуемся:

Пример с потоковой печатью и инициализацией по именам полей
//
//	Простая структура с методом печати в 
//	поток (например, Serial, SoftSerial, LCD и т.п.)
//
struct SBox : public Printable {
	int width;	//	ширина
	int length;	//	длина
	int height;	//	высота
	int weight;	//	вес

	size_t printTo(Print& p) const {
		size_t res = p.print("{ width: ");
		res += p.print(width);
		res += p.print("; length: ");
		res += p.print(length);
		res += p.print("; height: ");
		res += p.print(height);
		res += p.print("; weight: ");
		res += p.print(weight);
		res += p.println(" }");
		return res;
	}
};

//
// Структура проинициализированная по именам
//
SBox payLoad = {
	.width = 40, 
	.length = 200,
	.height = 32,
	.weight = 43
};

//
//	Просто печатаем структуру, чтобы посмотреть, 
//	как отработала инициализация 
//
void setup() {
	Serial.begin(9600);
	Serial.println(payLoad);
}

//	Здесь ничего не делаем
void loop() { delay(100500); }
Сообщения компилятора
D:\GoogleD\Soft\Uroki\StructInit\SturctInit2\SturctInit2.ino:33:1: error: could not convert '{40, 200, 32, 43}' from '<brace-enclosed initializer list>' to 'SBox'
 };
 ^

exit status 1

Compilation error: could not convert '{40, 200, 32, 43}' from '<brace-enclosed initializer list>' to 'SBox'```

и опять кушаем ошибку компиляции.

Ну, что, пока подход “по именам” выглядит гораздо лучше “безымянного”. По крайней мере, он ни разу не скомпилировал неправильно работающий код из-за нашей забывчивости. Т.е. всякий раз, когда мы меняем структуру и забываем поправить инициализацию, компилятор ругнётся и заставит на это сделать! Т.е. в целом, поход годный. Но, структуры с нетривиальными конструкторами (грабли №3) так инициализировать, по-прежнему, нельзя (а в наше время это неприлично), а, кроме того здесь нас подстерегают

Грабли №4

Такая инициализация не описана в стандарте языка – это расширение GCC. А это значит, что перейдя на другой компилятор, мы можем столкнуться с тем, что там это не работает :frowning:

Как это можно делать “в наше время”?

Главная идея: добавить в структуру конструктор. А если мы не планируем её инициализировать ничем, кроме известных во время компиляции констант (как во всех наши примерах), так лучше не просто конструктор, а constexpr конструктор!

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

Пример с потоковой печатью и конструктором
//
//	Простая структура с методом печати в 
//	поток (например, Serial, SoftSerial, LCD и т.п.)
//
struct SBox : public Printable {
	int length;	//	длина
	int height;	//	высота
	int width;	//	ширина
	int weight;	//	вес

	constexpr SBox(const int l, const int h, const int wi, const int we) : length(l), height(h), width(wi), weight(we) {}

	size_t printTo(Print& p) const {
		size_t res = p.print("{ width: ");
		res += p.print(width);
		res += p.print("; length: ");
		res += p.print(length);
		res += p.print("; height: ");
		res += p.print(height);
		res += p.print("; weight: ");
		res += p.print(weight);
		res += p.println(" }");
		return res;
	}
};

//
// Массив структур, каждый элемент инициализируем чуть по-разному
//
SBox payLoad(40, 200, 32, 43);
//
//	Кстати, если нужен массив, то с конструктором он инициализируется вот так:
//
SBox payLoadArray[] = {
	 SBox(40, 200, 32, 43),
	 SBox(34, 134, 16, 50),
	 SBox(32, 321, 24, 40)
};

//
//	Просто печатаем структуру, чтобы посмотреть, 
//	как отработала инициализация 
//
void setup() {
	Serial.begin(9600);
	Serial.println(payLoad);
}

//	Здесь ничего не делаем
void loop() { delay(100500); }
Результат работы примера
{ width: 32; length: 40; height: 200; weight: 43 }

Всё работает с потоковой печатью! Значит, грабли №3 здесь не действуют!

Проверим остальные грабли.

Грабли №1

Здесь всё интереснее, давайте смотреть внимательно:

Изменяем порядок следования полей структуры
//
//	Простая структура с методом печати в 
//	поток (например, Serial, SoftSerial, LCD и т.п.)
//
struct SBox : public Printable {
	int height;	//	высота
	int width;	//	ширина
	int length;	//	длина
	int weight;	//	вес

	constexpr SBox(const int l, const int h, const int wi, const int we) : length(l), height(h), width(wi), weight(we) {}

	size_t printTo(Print& p) const {
		size_t res = p.print("{ width: ");
		res += p.print(width);
		res += p.print("; length: ");
		res += p.print(length);
		res += p.print("; height: ");
		res += p.print(height);
		res += p.print("; weight: ");
		res += p.print(weight);
		res += p.println(" }");
		return res;
	}
};

//
// Массив структур, каждый элемент инициализируем чуть по-разному
//
SBox payLoad(40, 200, 32, 43);
//
//	Кстати, если нужен массив, то с конструктором он инициализируется вот так:
//
SBox payLoadArray[] = {
	 SBox(40, 200, 32, 43),
	 SBox(34, 134, 16, 50),
	 SBox(32, 321, 24, 40)
};

//
//	Просто печатаем структуру, чтобы посмотреть, 
//	как отработала инициализация 
//
void setup() {
	Serial.begin(9600);
	Serial.println(payLoad);
}

//	Здесь ничего не делаем
void loop() { delay(100500); }
Сообщения компилятора
D:\GoogleD\Soft\Uroki\StructInit\SturctInit1\SturctInit1.ino: In constructor 'constexpr SBox::SBox(int, int, int, int)':
D:\GoogleD\Soft\Uroki\StructInit\SturctInit1\SturctInit1.ino:8:6: warning: 'SBox::length' will be initialized after [-Wreorder]
  int length; // длина
      ^~~~~~
D:\GoogleD\Soft\Uroki\StructInit\SturctInit1\SturctInit1.ino:6:6: warning:   'int SBox::height' [-Wreorder]
  int height; // высота
      ^~~~~~
D:\GoogleD\Soft\Uroki\StructInit\SturctInit1\SturctInit1.ino:11:12: warning:   when initialized here [-Wreorder]
  constexpr SBox(const int l, const int h, const int wi, const int we) : length(l), height(h), width(wi), weight(we) {}
            ^~~~
Скетч использует 2232 байт (6%) памяти устройства. Всего доступно 32256 байт.
Глобальные переменные используют 250 байт (12%) динамической памяти, оставляя 1798 байт для локальных переменных. Максимум: 2048 байт.
Результат работы примера
{ width: 32; length: 40; height: 200; weight: 43 }

Итак, что произошло. Компилятор нас предупредил о том, что он будет инициализировать поля не в том порядке, как написано у нас в строке №11, а в том, в котором эти элементы объявлены в строках №№6-9.

Но наша программа отработала правильно (сравните результат работы с предыдущим примером!). Т.е. все поля получили те значения, которые мы и хотели присвоить!

Важно ли нам в каком порядке инициализируются поля (о чём нас предупредил компилятор)? В нашем пример – нет. Это было бы важно, если бы следующее поле инициализировалось бы с использованием значения предыдущего. Но, разумеется, лучше их там писать в правильном порядке.

Итог - программа отработала правильно. Считаем, что эти грабли неопасны!

Грабли №2

Добавление полей. Попробуйте сами взять изначальный код из данного раздела и добавить поле (куда угодно). Вас просто обругает компилятор и заставят его тоже проинициализировать в строке №11. Если пока не знаете чем – проинициализируйте нулём. Только порядок следования соблюдайте (см. обсуждение граблей №1 в этом разделе).

Ну, грабли №3 при таком походе отсутствуют (мы это уже заметили), грабли №4 - тоже, здесь всё стандартно.

Таким образом,

использование конструктора позволяет избежать всех граблей, какие мы здесь рассматривали. При этом, такой конструктор не стоит ничего в плане кода и/или памяти. Он отрабатывает во время компиляции и в код идёт просто проинициализированный кусок памяти - т.е. он нас требуется только написать единственную несложную строку (№11 в нашем примере), это совершенно бесплатно и это избавляет нас от граблей.

Ну, как-то так. Спасибо тем, кто “асилил”.

Дополнение от 11.09.2024:
простите дурака, совсем вылетело из головы, что в ISO/IEC 14882:2020 т.н. “назначенная инициализация” (с указанием имён полей) получила законную прописку. Так что о граблях №4 можно потихоньку забывать. Пока ещё не все реализации поддерживают стандарт 2020, но движение идёт в правильном направлении.

8 лайков

Ну как бы не совсем. Грабли номер 1 списочной инициализации (метод 1) ( то с чего вы начали обсуждение) -в полной мере возможны и в методе 3 - ведь конструктор то вы списком инициализируете.

По синтаксису эти два варианта практически неотличимы:

Списочная инициализация без конструктора

Она же с конструктором

От перестановки полей в списке конструктор никакой защиты не даёт( что очевидно).

Нет.

Грабли №1 в методе 3 рассмотрены с примером кода, сообщения и т.д. Если переставить местами элементы структуры (и не трогать ничего больше) - будет предупреждение о неверном порядке инициализации, но сам код сработает вполне штатно и результат будет правильный.

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

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

Здесь, видимо, пропущены какие-то окончания – не могу сказать, что я точно понял эту фразу, но Вы просто запустите пример граблей №1 из раздела про конструктор и посмотрите как он сработает, он ведь приведён не зря.

Видимо подразумевается, что в структуре поля переставили, конструктор подфиксили, а в середине 138-го файла инит остался тем же, безымянным. Значения попадут не в те переменные.

С инитом именованными полями тут хоть какая-то есть гарантия того, что по полочкам разложится.

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

Тем более, что найти их (вызовы) совсем нетрудно – убираю (комментирую) конструктор и компилятор тут же показывает мне ВСЕ места где он (конструктор) вызывается во всех 100500 файлах.

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

Пример списочной инициализации, который я процитировал, взят из вашего кода. Это первая вставка кода под заголовком Грабли 1 раздела " Как это можно делать “в наше время”?".

Строчки 32-38:

Я не вижу, что может помешать нам проинициализировать поля в другом порядке.

Что Вы понимаете под “проинициализировать поля в другом порядке”? Перепутать параметры конструктора? Если так, то грабли не про это. Грабли про “переставить местами поля структуры при её объявлении”. Возьмите пример, переставьте поля, запустите и убедитесь, что всё отработало штатно.

Это очень легко может произойти, если определение структуры с ее полями и конструктором - внешнее, а код, ее использующий - ваш. Вы в самом начале дали ссылку на старый форум, где у вас была именно такая проблема.
Есть код, который использует “старую” структуру, потом вышла новая версия с другим порядком полей, но с теми же типами. Старый код скомпилируется с новой библиотекой без проблем и вы никак не узнаете, что поля изменились.

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

Конструктор эту проблему, на мой взгляд, не снимает.

@ЕвгенийП огромный респект за Ваши уроки.
Меня заинтересовал такой вопрос: можно ли совместить инициализация конструктором с инициализацией по именам полей?
В качестве домашнего задания, попробовал немного модифицировать Ваш пример с конструктором.

Попытка №1
//
//	Простая структура с методом печати в 
//	поток (например, Serial, SoftSerial, LCD и т.п.)
//
struct SBox : public Printable {
	int length;	//	длина
	int height;	//	высота
	int width;	//	ширина
	int weight;	//	вес

	//constexpr SBox(const int l, const int h, const int wi, const int we) : length(l), height(h), width(wi), weight(we) {}
  constexpr SBox() : length(0), height(0), width(0), weight(0) {}
  constexpr SBox& Length(const int l) { length = l; return *this; }
  constexpr SBox& Height(const int h) { height = h; return *this; }
  constexpr SBox& Width(const int wi) { width = wi; return *this; }
  constexpr SBox& Weight(const int w) { weight = w; return *this; }

	size_t printTo(Print& p) const {
		size_t res = p.print("{ width: ");
		res += p.print(width);
		res += p.print("; length: ");
		res += p.print(length);
		res += p.print("; height: ");
		res += p.print(height);
		res += p.print("; weight: ");
		res += p.print(weight);
		res += p.println(" }");
		return res;
	}
};

//
// Массив структур, каждый элемент инициализируем чуть по-разному
//
SBox payLoad = SBox().Length(40).Height(200).Width(32).Weight(43);
//
//	Кстати, если нужен массив, то с конструктором он инициализируется вот так:
//
SBox payLoadArray[] = {
	 SBox().Length(40).Height(200).Width(32).Weight(43),
	 SBox().Length(34).Height(134).Width(16).Weight(50), //34, 134, 16, 50
	 SBox().Length(32).Height(321).Width(24).Weight(40) //32, 321, 24, 40
};

//
//	Просто печатаем структуру, чтобы посмотреть, 
//	как отработала инициализация 
//
void setup() {
	Serial.begin(9600);
	Serial.println(payLoad);
}

//	Здесь ничего не делаем
void loop() { delay(100500); }

Этот код не компилируется.

Сообщения об ошибках
C:\Users\WDAGUtilityAccount\Documents\Arduino\sketch_sep3b\sketch_sep3b.ino: In member function 'constexpr SBox& SBox::Length(int) const':
C:\Users\WDAGUtilityAccount\Documents\Arduino\sketch_sep3b\sketch_sep3b.ino:13:50: error: assignment of member 'SBox::length' in read-only object
   constexpr SBox& Length(const int l) { length = l; return *this; }
                                                  ^
C:\Users\WDAGUtilityAccount\Documents\Arduino\sketch_sep3b\sketch_sep3b.ino:13:60: error: binding reference of type 'SBox&' to 'const SBox' discards qualifiers
   constexpr SBox& Length(const int l) { length = l; return *this; }
                                                            ^~~~~
C:\Users\WDAGUtilityAccount\Documents\Arduino\sketch_sep3b\sketch_sep3b.ino:13:67: error: body of constexpr function 'constexpr SBox& SBox::Length(int) const' not a return-statement
   constexpr SBox& Length(const int l) { length = l; return *this; }
                                                                   ^
C:\Users\WDAGUtilityAccount\Documents\Arduino\sketch_sep3b\sketch_sep3b.ino: In member function 'constexpr SBox& SBox::Height(int) const':
C:\Users\WDAGUtilityAccount\Documents\Arduino\sketch_sep3b\sketch_sep3b.ino:14:50: error: assignment of member 'SBox::height' in read-only object
   constexpr SBox& Height(const int h) { height = h; return *this; }
                                                  ^
C:\Users\WDAGUtilityAccount\Documents\Arduino\sketch_sep3b\sketch_sep3b.ino:14:60: error: binding reference of type 'SBox&' to 'const SBox' discards qualifiers
   constexpr SBox& Height(const int h) { height = h; return *this; }
                                                            ^~~~~
C:\Users\WDAGUtilityAccount\Documents\Arduino\sketch_sep3b\sketch_sep3b.ino:14:67: error: body of constexpr function 'constexpr SBox& SBox::Height(int) const' not a return-statement
   constexpr SBox& Height(const int h) { height = h; return *this; }
                                                                   ^
C:\Users\WDAGUtilityAccount\Documents\Arduino\sketch_sep3b\sketch_sep3b.ino: In member function 'constexpr SBox& SBox::Width(int) const':
C:\Users\WDAGUtilityAccount\Documents\Arduino\sketch_sep3b\sketch_sep3b.ino:15:49: error: assignment of member 'SBox::width' in read-only object
   constexpr SBox& Width(const int wi) { width = wi; return *this; }
                                                 ^~
C:\Users\WDAGUtilityAccount\Documents\Arduino\sketch_sep3b\sketch_sep3b.ino:15:60: error: binding reference of type 'SBox&' to 'const SBox' discards qualifiers
   constexpr SBox& Width(const int wi) { width = wi; return *this; }
                                                            ^~~~~
C:\Users\WDAGUtilityAccount\Documents\Arduino\sketch_sep3b\sketch_sep3b.ino:15:67: error: body of constexpr function 'constexpr SBox& SBox::Width(int) const' not a return-statement
   constexpr SBox& Width(const int wi) { width = wi; return *this; }
                                                                   ^
C:\Users\WDAGUtilityAccount\Documents\Arduino\sketch_sep3b\sketch_sep3b.ino: In member function 'constexpr SBox& SBox::Weight(int) const':
C:\Users\WDAGUtilityAccount\Documents\Arduino\sketch_sep3b\sketch_sep3b.ino:16:50: error: assignment of member 'SBox::weight' in read-only object
   constexpr SBox& Weight(const int w) { weight = w; return *this; }
                                                  ^
C:\Users\WDAGUtilityAccount\Documents\Arduino\sketch_sep3b\sketch_sep3b.ino:16:60: error: binding reference of type 'SBox&' to 'const SBox' discards qualifiers
   constexpr SBox& Weight(const int w) { weight = w; return *this; }
                                                            ^~~~~
C:\Users\WDAGUtilityAccount\Documents\Arduino\sketch_sep3b\sketch_sep3b.ino:16:67: error: body of constexpr function 'constexpr SBox& SBox::Weight(int) const' not a return-statement
   constexpr SBox& Weight(const int w) { weight = w; return *this; }
                                                                   ^

exit status 1

Compilation error: assignment of member 'SBox::length' in read-only object

Если убрать constexpr из обявления функций инициализации полей, код скомпилируется, но увеличивается размер прошивки.

Попытка №2
//
//	Простая структура с методом печати в 
//	поток (например, Serial, SoftSerial, LCD и т.п.)
//
struct SBox : public Printable {
	int length;	//	длина
	int height;	//	высота
	int width;	//	ширина
	int weight;	//	вес

	//constexpr SBox(const int l, const int h, const int wi, const int we) : length(l), height(h), width(wi), weight(we) {}
  constexpr SBox() : length(0), height(0), width(0), weight(0) {}
  SBox& Length(const int l) { length = l; return *this; }
  SBox& Height(const int h) { height = h; return *this; }
  SBox& Width(const int wi) { width = wi; return *this; }
  SBox& Weight(const int w) { weight = w; return *this; }

	size_t printTo(Print& p) const {
		size_t res = p.print("{ width: ");
		res += p.print(width);
		res += p.print("; length: ");
		res += p.print(length);
		res += p.print("; height: ");
		res += p.print(height);
		res += p.print("; weight: ");
		res += p.print(weight);
		res += p.println(" }");
		return res;
	}
};

//
// Массив структур, каждый элемент инициализируем чуть по-разному
//
SBox payLoad = SBox().Length(40).Height(200).Width(32).Weight(43);
//
//	Кстати, если нужен массив, то с конструктором он инициализируется вот так:
//
SBox payLoadArray[] = {
	 SBox().Length(40).Height(200).Width(32).Weight(43),
	 SBox().Length(34).Height(134).Width(16).Weight(50), //34, 134, 16, 50
	 SBox().Length(32).Height(321).Width(24).Weight(40) //32, 321, 24, 40
};

//
//	Просто печатаем структуру, чтобы посмотреть, 
//	как отработала инициализация 
//
void setup() {
	Serial.begin(9600);
	Serial.println(payLoad);
}

//	Здесь ничего не делаем
void loop() { delay(100500); }

Можно ли как нибудь проделать подобный трюк без увеличения размера кода?

Вообще-то не удивительно.
У вас ведь не “инициализация конструктором” получилась, а последовательный вызов конструктора и потом четырех сеттеров.

Спасибо, я в курсе, что получилось. Вопрос был - можно ли переложить последовательный вызов функций инициализации на компилятор или такой возможности нет в принципе.

@nibelung - попробовал тоже ответить на ваш вопрос:

Можно изобразить что похожее, задав каждому аргументу в конструкторе

SBox(const int l, const int h, const int wi, const int we)

свой собственный тип вместо стандартного int,
При этом члены структуры оставим int, чтобы не перелопачивать код.

// Специальные типы данных для каждого параметра
struct customInt
{
    int x;
    customInt(const int l) : x(l) {}
    int toInt() {return x;}
};
struct Length : public customInt { using customInt::customInt; };
struct Height : public customInt { using customInt::customInt; };
struct Width : public customInt { using customInt::customInt; };
struct Weight : public customInt { using customInt::customInt; };

//
//  Простая структура с методом печати в 
//  поток (например, Serial, SoftSerial, LCD и т.п.)
//
struct SBox : public Printable {
  int length; //  длина
  int height; //  высота
  int width;  //  ширина
  int weight; //  вес

  SBox(const Length l, const Height h, const Width wi, const Weight we) : length(l.toInt()), height(h.toInt()), width(wi.toInt()), weight(we.toInt()) {}
  
  size_t printTo(Print& p) const {
    size_t res = p.print("{ width: ");
    res += p.print(width);
    res += p.print("; length: ");
    res += p.print(length);
    res += p.print("; height: ");
    res += p.print(height);
    res += p.print("; weight: ");
    res += p.print(weight);
    res += p.println(" }");
    return res;
  }
};

//
// Массив структур, каждый элемент инициализируем чуть по-разному
//

SBox payLoad( (Length){200}, (Height){2},   (Width){32},  Weight {43});

//
//  Просто печатаем структуру, чтобы посмотреть, 
//  как отработала инициализация 
//
void setup() {
  Serial.begin(9600);
  Serial.println(payLoad);
}

//  Здесь ничего не делаем
void loop() { delay(100500); }

Теперь каждый аргумент в вызове конструктора - типизованный:

SBox payLoad( (Length){200}, (Height){2}, (Width){32}, Weight {43});

если поменять их порядок - не скомпилируется.

Размер кода увеличился при этом всего на 30 байт.
Не знаю, говнокод это или нет, надеюсь @ЕвгенийП скажет

1 лайк

Дело в том, что не только полей! Если только полей, то проблемы нет никакой, от слова совсем. Где Вы увидели проблему?

Вот смотрите ещё раз. Вот это изначальный код:

Изначальный код (№1)
//
//	Простая структура с методом печати в 
//	поток (например, Serial, SoftSerial, LCD и т.п.)
//
struct SBox : public Printable {
	int length;	//	длина
	int height;	//	высота
	int width;	//	ширина
	int weight;	//	вес
	constexpr SBox(const int l, const int h, const int wi, const int we) : length(l), height(h), width(wi), weight(we) {}

	size_t printTo(Print& p) const {
		size_t res = p.print("{ width: ");
		res += p.print(width);
		res += p.print("; length: ");
		res += p.print(length);
		res += p.print("; height: ");
		res += p.print(height);
		res += p.print("; weight: ");
		res += p.print(weight);
		res += p.println(" }");
		return res;
	}
};

//
// Массив структур, каждый элемент инициализируем чуть по-разному
//
SBox payLoad(40, 200, 32, 43);
//
//	Кстати, если нужен массив, то с конструктором он инициализируется вот так:
//
SBox payLoadArray[] = {
	 SBox(40, 200, 32, 43),
	 SBox(34, 134, 16, 50),
	 SBox(32, 321, 24, 40)
};

//
//	Просто печатаем структуру, чтобы посмотреть, 
//	как отработала инициализация 
//
void setup() {
	Serial.begin(9600);
	Serial.println(payLoad);
}

//	Здесь ничего не делаем
void loop() { delay(100500); }
Сообщения компилятора
Скетч использует 2232 байт (6%) памяти устройства. Всего доступно 32256 байт.
Глобальные переменные используют 250 байт (12%) динамической памяти, оставляя 1798 байт для локальных переменных. Максимум: 2048 байт.
Результат работы примера
{ width: 32; length: 40; height: 200; weight: 43 }

Теперь поменяем порядок описания полей:

Код с изменённым порядком описания полей в строках 6-9 (№2)
//
//	Простая структура с методом печати в 
//	поток (например, Serial, SoftSerial, LCD и т.п.)
//
struct SBox : public Printable {
	int height;	//	высота
	int width;	//	ширина
	int length;	//	длина
	int weight;	//	вес
	constexpr SBox(const int l, const int h, const int wi, const int we) : length(l), height(h), width(wi), weight(we) {}

	size_t printTo(Print& p) const {
		size_t res = p.print("{ width: ");
		res += p.print(width);
		res += p.print("; length: ");
		res += p.print(length);
		res += p.print("; height: ");
		res += p.print(height);
		res += p.print("; weight: ");
		res += p.print(weight);
		res += p.println(" }");
		return res;
	}
};

//
// Массив структур, каждый элемент инициализируем чуть по-разному
//
SBox payLoad(40, 200, 32, 43);
//
//	Кстати, если нужен массив, то с конструктором он инициализируется вот так:
//
SBox payLoadArray[] = {
	 SBox(40, 200, 32, 43),
	 SBox(34, 134, 16, 50),
	 SBox(32, 321, 24, 40)
};

//
//	Просто печатаем структуру, чтобы посмотреть, 
//	как отработала инициализация 
//
void setup() {
	Serial.begin(9600);
	Serial.println(payLoad);
}

//	Здесь ничего не делаем
void loop() { delay(100500); }
Сообщения компилятора
D:\GoogleD\Soft\Uroki\StructInit\SturctInit1\SturctInit1.ino: In constructor 'constexpr SBox::SBox(int, int, int, int)':
D:\GoogleD\Soft\Uroki\StructInit\SturctInit1\SturctInit1.ino:8:6: warning: 'SBox::length' will be initialized after [-Wreorder]
  int width; // ширина
      ^~~~~~
D:\GoogleD\Soft\Uroki\StructInit\SturctInit1\SturctInit1.ino:6:6: warning:   'int SBox::height' [-Wreorder]
  int length; // длина
      ^~~~~~
D:\GoogleD\Soft\Uroki\StructInit\SturctInit1\SturctInit1.ino:10:12: warning:   when initialized here [-Wreorder]
  constexpr SBox(const int l, const int h, const int wi, const int we) : length(l), height(h), width(wi), weight(we) {}
            ^~~~
Скетч использует 2232 байт (6%) памяти устройства. Всего доступно 32256 байт.
Глобальные переменные используют 250 байт (12%) динамической памяти, оставляя 1798 байт для локальных переменных. Максимум: 2048 байт.
Результат работы примера
{ width: 32; length: 40; height: 200; weight: 43 }

Как видите:

  1. мы поменяли порядок описания полей
  2. компилятор выдал предупреждение
  3. но код отработал правильно!

Хорошо, теперь давайте избавимся от предупреждений компилятора, для этого поменяем порядок инициализации:

Код с изменённым порядком описания членов в строках 6-9 и поправленным порядком иницализации в строке 10 (№3)
//
//	Простая структура с методом печати в 
//	поток (например, Serial, SoftSerial, LCD и т.п.)
//
struct SBox : public Printable {
	int height;	//	высота
	int width;	//	ширина
	int length;	//	длина
	int weight;	//	вес
	constexpr SBox(const int l, const int h, const int wi, const int we) : height(h), width(wi), length(l), weight(we) {}

	size_t printTo(Print& p) const {
		size_t res = p.print("{ width: ");
		res += p.print(width);
		res += p.print("; length: ");
		res += p.print(length);
		res += p.print("; height: ");
		res += p.print(height);
		res += p.print("; weight: ");
		res += p.print(weight);
		res += p.println(" }");
		return res;
	}
};

//
// Массив структур, каждый элемент инициализируем чуть по-разному
//
SBox payLoad(40, 200, 32, 43);
//
//	Кстати, если нужен массив, то с конструктором он инициализируется вот так:
//
SBox payLoadArray[] = {
	 SBox(40, 200, 32, 43),
	 SBox(34, 134, 16, 50),
	 SBox(32, 321, 24, 40)
};

//
//	Просто печатаем структуру, чтобы посмотреть, 
//	как отработала инициализация 
//
void setup() {
	Serial.begin(9600);
	Serial.println(payLoad);
}

//	Здесь ничего не делаем
void loop() { delay(100500); }
Сообщения компилятора
Скетч использует 2232 байт (6%) памяти устройства. Всего доступно 32256 байт.
Глобальные переменные используют 250 байт (12%) динамической памяти, оставляя 1798 байт для локальных переменных. Максимум: 2048 байт.
Результат работы примера
{ width: 32; length: 40; height: 200; weight: 43 }

Что мы видим? Предупреждения компилятора ушли, а код по прежнему отрабатывает правильно!

Чтобы свалить этот код необходимо явно изменить порядок следования параметров конструктора! Никакими другими изменениями этот код не валится.

Так что мои грабли №1 этот код вполне выдерживает.

Изменение порядка следования параметров конструктора – совсем другие грабли. Они здесь не обсуждались потому, что я с трудом могу себе представить программиста, который, изменяя порядок следования параметров функции, забывает поправить все места, где эта функция вызывается. Случайно так ошибиться невозможно. Если ж это чужой конструктор … ну, значит, не судьба. Хотя, бороться с неверным порядком следования параметров тоже можно, но это уже другая история.

В этом сообщении я пронумеровал все коды, чтобы Вы могли ссылаться. И, если Вы захотите показать мне, что оно всё-таки валится – пожалуйста, тоже с примером.

Спасибо за ответ.

Именно про этот случай я и говорил. Ко всем остальным рассмотренным вариантам у меня претензий нет.

На этом заканчиваю приставать, чтобы не засорять ветку.

Конечно, и не одним способом. Я такие фокусы не стал рассматривать в тексте по двум причинам - а) сложно и б) нестандартно (грабли №4).

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

Код, совмещающий поимённую инициализацию и конструктор, так что работает потоковый вывод
//
//	Простая структура с методом печати в 
//	поток (например, Serial, SoftSerial, LCD и т.п.)
//
struct SBoxBase {
	int width;	//	ширина
	int length;	//	длина
	int height;	//	высота
	int weight;	//	вес
};

struct SBox : public Printable {
	int height;	//	высота
	int width;	//	ширина
	int length;	//	длина
	int weight;	//	вес
	
	constexpr SBox(const SBoxBase & sb) : 
		height(sb.height), 
		width(sb.width), 
		length(sb.length), 
		weight(sb.weight) {}

	size_t printTo(Print& p) const {
		size_t res = p.print("{ width: ");
		res += p.print(width);
		res += p.print("; length: ");
		res += p.print(length);
		res += p.print("; height: ");
		res += p.print(height);
		res += p.print("; weight: ");
		res += p.print(weight);
		res += p.println(" }");
		return res;
	}
};

//
// Инициализация структуры
//
SBox payLoad(
	SBoxBase ({
		.width = 40, 
		.length = 200,
		.height = 32,
		.weight = 43
	}) 
);

//
//	Просто печатаем структуру, чтобы посмотреть, 
//	как отработала инициализация 
//
void setup() {
	Serial.begin(9600);
	Serial.println(payLoad);
}

//	Здесь ничего не делаем
void loop() { delay(100500); }

Запускайте, работает.

Можно сделать красивее, но это через “стандартную библиотеку” при помощью std::initializer_list, но у нас в IDE это не поддерживается. Обойти можно, но я никогда не думал как. Надо подумать, но то, что “можно” – сто пудов.

2 лайка

Поскольку дискуссия возникла, я напишу в тему. Жаль, что не отметился Ркит, как именно практикующий программист.
Дело в том, что инициализация в фигурных скобках не приветствуется вообще отраслевыми стандартами. MISRA разрешает её, конечно, но с точным соблюдением всех уровней скобок.
Все равно “чистый код” - это либо инициализация с именами полей или конструктор.

Причем было бы очень здорово, и разговор про это давно идет, реализовать в С++ такой же способ вызова функций, как в Python и прочих языках - с named arguments.
ПРИМЕР

void init (int index, double value= 0.0, const char * name = "nomame") {
//... some code
}

//call old stile
init (1, 12, "first");
//new stile
init (name = "second", index = 2)
// if name omitted use defalt value for argument
// порядок следования не имеет значения

Вот тогда инициализация через конструктор станет совершенной.

Я далек от дискуссий, но автор ветки, вероятно сможет уточнить про введение именованных аргументов в С++. Думаю, что скоро. В принципе, “через жопу” это можно сделать средствами, уже имеющимися в языке.

Идея как идея, но есть несколько замечаний:

Обсуждаемый код
// Специальные типы данных для каждого параметра
struct customInt
{
    int x;
    customInt(const int l) : x(l) {}
    int toInt() {return x;}
};
struct Length : public customInt { using customInt::customInt; };
struct Height : public customInt { using customInt::customInt; };
struct Width : public customInt { using customInt::customInt; };
struct Weight : public customInt { using customInt::customInt; };

//
//  Простая структура с методом печати в 
//  поток (например, Serial, SoftSerial, LCD и т.п.)
//
struct SBox : public Printable {
  int length; //  длина
  int height; //  высота
  int width;  //  ширина
  int weight; //  вес

  SBox(const Length l, const Height h, const Width wi, const Weight we) : length(l.toInt()), height(h.toInt()), width(wi.toInt()), weight(we.toInt()) {}
  
  size_t printTo(Print& p) const {
    size_t res = p.print("{ width: ");
    res += p.print(width);
    res += p.print("; length: ");
    res += p.print(length);
    res += p.print("; height: ");
    res += p.print(height);
    res += p.print("; weight: ");
    res += p.print(weight);
    res += p.println(" }");
    return res;
  }
};

//
// Массив структур, каждый элемент инициализируем чуть по-разному
//

SBox payLoad( (Length){200}, (Height){2},   (Width){32},  Weight {43});

//
//  Просто печатаем структуру, чтобы посмотреть, 
//  как отработала инициализация 
//
void setup() {
  Serial.begin(9600);
  Serial.println(payLoad);
}

//  Здесь ничего не делаем
void loop() { delay(100500); }

Замечание №1

Компилятор ругается (предупреждает)
...\SturctInit1.ino: In constructor 'SBox::SBox(Length, Height, Width, Weight)':
...\SturctInit1.ino:23:90: warning: passing 'const Length' as 'this' argument discards qualifiers [-fpermissive]
   SBox(const Length l, const Height h, const Width wi, const Weight we) : length(l.toInt()), height(h.toInt()), width(wi.toInt()), weight(we.toInt()) {}
                                                                                          ^
...\SturctInit1.ino:6:9: note:   in call to 'int customInt::toInt()'
     int toInt() {return x;}
         ^~~~~
...\SturctInit1.ino:23:109: warning: passing 'const Height' as 'this' argument discards qualifiers [-fpermissive]
   SBox(const Length l, const Height h, const Width wi, const Weight we) : length(l.toInt()), height(h.toInt()), width(wi.toInt()), weight(we.toInt()) {}
                                                                                                             ^
...\SturctInit1.ino:6:9: note:   in call to 'int customInt::toInt()'
     int toInt() {return x;}
         ^~~~~
...\SturctInit1.ino:23:128: warning: passing 'const Width' as 'this' argument discards qualifiers [-fpermissive]
   SBox(const Length l, const Height h, const Width wi, const Weight we) : length(l.toInt()), height(h.toInt()), width(wi.toInt()), weight(we.toInt()) {}
                                                                                                                                ^
...\SturctInit1.ino:6:9: note:   in call to 'int customInt::toInt()'
     int toInt() {return x;}
         ^~~~~
...\SturctInit1.ino:23:148: warning: passing 'const Weight' as 'this' argument discards qualifiers [-fpermissive]
   SBox(const Length l, const Height h, const Width wi, const Weight we) : length(l.toInt()), height(h.toInt()), width(wi.toInt()), weight(we.toInt()) {}
                                                                                                                                                    ^
...\SturctInit1.ino:6:9: note:   in call to 'int customInt::toInt()'
     int toInt() {return x;}
         ^~~~~
Скетч использует 2266 байт (7%) памяти устройства. Всего доступно 32256 байт.
Глобальные переменные используют 250 байт (12%) динамической памяти, оставляя 1798 байт для локальных переменных. Максимум: 2048 байт.

Но это нетрудно исправить достаточно добавить слово const в строку №6, чтобы она стала:

    int toInt() const {return x;}

Всё, ругань ушла.

Замечание №2

Про то, что

Это тоже легко исправимо. Тут легко экономится даже чуть больше. Вы напрасно отказались от constexpr. Я вернул constexpr на место (попутно заменив Вашу toInt на оператор приведения типа), смотрите:

Примерно то же самое, но с constexpr
// Специальные типы данных для каждого параметра
struct customInt
{
    int x;
    constexpr customInt(const int l) : x(l) {}
	 constexpr operator int() const { return x; }
//    int toInt() const {return x;}
};
struct Length : public customInt { using customInt::customInt; };
struct Height : public customInt { using customInt::customInt; };
struct Width : public customInt { using customInt::customInt; };
struct Weight : public customInt { using customInt::customInt; };

//
//  Простая структура с методом печати в 
//  поток (например, Serial, SoftSerial, LCD и т.п.)
//
struct SBox : public Printable {
  int length; //  длина
  int height; //  высота
  int width;  //  ширина
  int weight; //  вес

  constexpr SBox(const Length l, const Height h, const Width wi, const Weight we) : length(l), height(h), width(wi), weight(we) {}
  
  size_t printTo(Print& p) const {
    size_t res = p.print("{ width: ");
    res += p.print(width);
    res += p.print("; length: ");
    res += p.print(length);
    res += p.print("; height: ");
    res += p.print(height);
    res += p.print("; weight: ");
    res += p.print(weight);
    res += p.println(" }");
    return res;
  }
};

//
// Массив структур, каждый элемент инициализируем чуть по-разному
//

SBox payLoad( (Length){200}, (Height){2},   (Width){32},  Weight {43});

//
//  Просто печатаем структуру, чтобы посмотреть, 
//  как отработала инициализация 
//
void setup() {
  Serial.begin(9600);
  Serial.println(payLoad);
}

//  Здесь ничего не делаем
void loop() { delay(100500); }

Сравните размеры. 32 байта кода как корова языком слизнула

2 лайка
2 лайка

Пока такие ООП-инициализаторы наклацаешь - забудешь зачем IDE открыл ))

1 лайк

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