Как стать автором
Обновить

Синтаксис, синглтон и смертельный ромб в С++: взгляд опытного разработчика на C

Уровень сложностиСредний
Время на прочтение15 мин
Количество просмотров9.8K
Всего голосов 50: ↑46 и ↓4+54
Комментарии36

Комментарии 36

С одной стороны круто, автор - молодец.

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

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

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

Знать такие тонкости языка

При беглом просмотре статьи вообще никаких тонкостей не обнаружилось.

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

правило 5 вполне себе тонкости

Назвать правило 5 тонкостями не могу при всем уважении. Это база, которую нужно знать, чтобы нормально писать на современном C++.

Тонкостью для того же правила 5 можно было бы назвать наличие noexcept на операторах/конструкторах копирования/перемещения (или отсутствия этого самого noexcept). ИМХО.

Если хочется тонкостей, то на сайте PVS-Studio сейчас публикуется серия статей про UB в Си и C++. Вот там тонкостей разной степени тонкости в ассортименте.

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

В Расте тонкости обычно класса "да как же заставить этот @!#?@! код компилироваться?!" и если начать копать, почему же именно он не компилируется и как это исправить, то кроличья нора может оказаться весьма глубока. Если безопасный Раст скомпилировался, то он почти наверняка работает как надо.

Если безопасный Раст скомпилировался, то он почти наверняка работает как надо.

не сильно опытен в расте, но довольно спорное суждение)

С опытом написания на C++ я избегаю 99% UB, но это никак не помогает мне избавиться от логических ошибок. Также не поможет и раст.

Точно 99% избегаете? При том, что одной хватит, чтобы разрушить всю программу.

Если более серьезно, то на мой взгляд основной источник ошибок – работа с указателями/ссылками, обработка ошибок и внезапный null (в языках, которые это разрешают), и многопоточность. От этих вещей Раст защищает неплохо. Почти то же самое могу сказать про ТайпСкрипт – если оно протайпчекалось, то undefined is not a function в рантайме, скорее всего, не полетит (но тайпчекер там очень легко заткнуть, к сожалению).

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

Тонкости языка это ладно. Основная проблема плюсов, на мой взгляд, в том, что практически никто из людей не может писать на плюсах без UB. Ну то есть кто-то, наверное, может, есть люди которые стометровку за десять секунд бегают, но вот, например, Дедфуд, Великий и Ужасный, в своих способностях писать на C++ без UB не уверен. А если он не может, то что уж говорить об остальных...

Это разработчики компиляторов ломают опыт. В самом языке UB нет.

В стандарте языка они есть, увы и ах. Вопрос со звёздочкой: зачем их ввели в стандарт?

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

Именно так, ради производительности, скорость выполнения очень важна для C++ кода. В железе, конечно, гораздо больше implementation defined (и меняется, вообще говоря, с выходом каждого процессора), но там своя кроличья нора оптимизации под капотом, Spectre/Meltdown тому подтверждение. Хорошая статья на тему: https://queue.acm.org/detail.cfm?id=3212479 , там про C, но к C++ все полностью применимо.

по-моему С++ зашел куда-то не туда.

кажется я придумал адекватное объяснение этому "кажется", то есть почему нам так кажется, читаем:

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

У Тимура Думлера есть замечательная таблица, которая ...

тут может возникнуть много интересных вопросов:

  1. сколько человек из 100 пишущих на С++ знают что есть такая таблица?

  2. сколько помнят что есть такая таблица?

  3. сколько ВСпомнят что есть такая таблица в нужный момент (нагруженные проблемами логики предметной области, например)?

  4. сколько помнят что есть такая таблица и знают ее наизусть?

  5. и самый главный вопрос: сколько всего существует подобных таблиц для разных С-плюсовых конструкций и сколько штук таких таблиц надо держать в голове и сколько штук человек СПОСОБЕН держать в голове, ...

Хорошая статья!

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

В 90х, начале 2000х на С++98 таким в массе не заморачивались. Инструмент резко усложнился.

Да и судя по всему с жавой аналогичная история случилось. Язык задумывался как более простой язык для бизнес-логики, более простая и безопасная альтернатива С++. Бери и пиши. А по факту на сегодня надо знать как там сборщик мусора под капотом работает в деталях, знать кучу деталей устройства под капотом работы компилятора и т.п. что бы считаться жуниор-программистом на жаве. Но ведь оно задумывалось так, что бы получать результат не углубляясь так.

Я не претендую, не пишу с середины 200х, но слежу за трендами. И тренды лично мне не нравятся.

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

Успокойтесь, это не обязательно.

надо обязательно знать во что оно именно компилируется в асме

Все ещё хуже. Можно посмотреть ассемблерный код, убедиться, что все в порядке, а потом меняется один трёх десятков флагов компилятора, и программа начинает выполнять rm -rf /. Почему? А вон там где-то есть UB, и компилятор по Стандарту может выдавать в принципе любой код в таком случае.

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

Полностью согласен: корректна программа на C++ или нет можно понять исключительно по ее исходному коду. Ассемблерный код в этом совершенно бесполезен.

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

Языков, в которых необыкновенно легко допустить UB, лучше избегать.

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

Отмечу что приведенный код "синглтона Майерса" может неправильно работать в многопоточной среде. После захвата мьютекса нужно проверить значение переменной __guard еще раз. Иначе при "одновременном" вызове функции f конструктор класса MyClass будет вызван несколько раз.

И использование pthread_mutex_t это скорее стиль языка C. В стандартной библиотеке C++ есть mutex и lock_guard.

Там ешё и UB дважды: нет выравнивания под MyClass у __storage, нет std::launder после reinterpret_cast (см. недавнее предложение в стандарт от Антона Полухина об убирании UB в таких случаях).

Согласен, к тому же указатели на "старые" экземпляры MyClass будут утекать. Но тут автор говорит, что это псевдокод для понимания принципа работы static. В реальном коде такого не хотелось бы видеть :)

UPD: Да, не заметил, что там placement new и никаких указателей там нет.

Комментарий ревьюера: «Ты давай так не делай. Ты лучше просто точку с запятой оставь, а фигурные скобки убери отсюда».

Странная претензия.

Если у нас есть класс, а не built-in тип и я хочу позвать дефолтный конструктор, то не надо использовать пустые фигурные скобки. Но, с другой стороны, если у меня built-in тип и я хочу, чтобы integer инициализировался нулем, то почему бы не использовать фигурные скобки, которые пойдут в value initialization, а value initialization приведет к zero initialization.

И выводы не менее странные.

Странная претензия.

Ну почему странная, эти скобки тут реально ничего полезного не делают, зачем их ставить. Вот в обобщенном коде, где тип заранее неизвестен, это может быть действительно большая разница, а тут-то? Вопрос вкуса и локальных coding conventions.

Вот в обобщенном коде, где тип заранее неизвестен, это может быть действительно большая разница, а тут-то?

Если взять за правило ставить {} для вызова дефолтного конструктора, то не будет разницы пишешь ты обобщенный код или конкретный. Особенно если конкретный со временем трансформируется в обобщенный или наоборот.

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

Полезность в том, что человек привык везде ставить {}, поэтому не забывает их поставить и для базовых типов.
А другой человек ставит то {}, то ; ("а зачем нам тут скобки?"). Рано или поздно этот человек забудет поставить {} для базового типа и словит UB.
Пример из реальной жизни: соседняя команда словила баг из-за неинициализированной переменной. В нашей же команде таких багов не бывает, потому что мы договорились везде ставить {} и не пропускать на ревью инициализацию с ;

Good practice

Тут также может зависеть от реализации конструкторов.

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

struct A{
   A(string s):m_s(move(s)){}

   string m_s{};
};

В одном классе я добавил ключевое слово final.

Это — не ключевое слово:

#include <cstdlib>
#include <iostream>

typedef struct final final {
	virtual ~final() final {
	}
} final;

int main() {
	std::cout << sizeof final{} << std::endl;
	return EXIT_SUCCESS;
}

Видите, какое хулиганство?

А всё — потому, что final — не ключевое слово.

Ехал файнал через файнал,

Видит файнал – в файнал файнал

Сунул файнал файнал в файнал,

Файнал файнал файнал файнал.

Можно ещё добавлю (код) ревью?

class MyInterface
{
public:
virtual void f() = 0;
virtual void g() = 0;
};

Надо бы добавить виртуальный деструктор.

// what??? Does it work in such way?

Мы так не говорим. Does it work in this way? Или, Does it work like this?

static bool __guard = false;
...
if (!__guard ) {
pthread_mutex_lock(__guard_mutex);
__guard = true;

Здесь не хватает барьеров для переменной __guard. Но проще всего std::atomicиспользовать.

Зарегистрируйтесь на Хабре, чтобы оставить комментарий