В последние годы C++ шагает вперед семимильными шагами, и угнаться за всеми тонкостями и хитросплетениями языка бывает весьма и весьма непросто. Уже не за горами новый стандарт, однако внедрение свежих веяний — процесс не самый быстрый и простой, поэтому, пока есть немного времени перед C++20, предлагаю освежить в памяти или открыть для себя некоторые особо «скользкие» места актуального на данный момент стандарта языка.
Сегодня я расскажу: почему if constexpr не является заменой макросов, каковы «внутренности» работы структурного связывания (structured binding) и его «подводные» камни и правда ли, что теперь всегда работает copy elision и можно не задумываясь писать любой return.
Если не боишься немного «испачкать» руки, копаясь во «внутренностях» языка, добро пожаловать под кат.
if constexpr
Начнём, пожалуй, с самого простого —
if constexpr
позволяет еще на этапе компиляции отбросить ветку условного выражения, для которой желаемое условие не выполняется. Кажется, что это замена макросу
#if
для выключения «лишней» логики? Нет. Совсем нет. Во-первых, такой
if
обладает свойствами, недоступными для макросов, — внутри можно посчитать любое constexpr
выражение, приводимое к bool
. Ну а во-вторых, содержимое отбрасываемой ветки должно быть синтаксически и семантически корректным. Из-за второго требования внутри
if constexpr
нельзя использовать, например, несуществующие функции (таким способом нельзя явно разделять платформо-зависимый код) или плохие с точки зрения языка конструкции (например « void T = 0;
»).В чем же тогда смысл использования
if constexpr
? Основной смысл — в шаблонах. Для них есть специальное правило: отбрасываемая ветка не инстанцируется при инстанцировании шаблона. Это позволяет проще писать код, который каким-то образом зависит от свойств шаблонных типов.Однако и в шаблонах нельзя забывать о том, что код внутри веток должен быть корректным хотя бы для какого-нибудь (даже чисто потенциального) варианта инстанцирования, поэтому просто написать, например,
static_assert(false)
внутри одной из веток нельзя (нужно, чтобы этот static_assert
зависел от какого-либо зависимого от шаблона параметра).Примеры:
void foo()
{
// в обеих ветках ошибки, поэтому не скомпилируется
if constexpr ( os == OS::win ) {
win_api_call(); // под другими платформами будет ошибка
}
else {
some_other_os_call(); // под win будет ошибка
}
}
template<class T>
void foo()
{
// Отбрасываемая ветка не инстанцируется, поэтому при правильном T код соберется
if constexpr ( os == OS::win ) {
T::win_api_call(); // если T поддерживает такой вызов, то ок под win
}
else {
T::some_other_os_call(); // если T поддерживает такой вызов, то ок под другую платформу
}
}
template<class T>
void foo()
{
if constexpr (condition1) {
// ...
}
else if constexpr (condition2) {
// ...
}
else {
// static_assert(false); // так нельзя
static_assert(trait<T>::value); // можно, даже при том, что trait<T>::value всегда будет false
}
}
О чём нужно помнить
- Код во всех ветках должен быть корректным.
- Внутри шаблонов содержимое отбрасываемых веток не инстанцируется.
- Код внутри любой ветки должен быть корректным хотя бы для одного чисто потенциального варианта инстанцирования шаблона.
Структурное связывание (structured binding)
В C++17 появился достаточно удобный механизм декомпозиции различных кортежеподобных объектов, позволяющий удобно и лаконично привязывать их внутренние элементы к именованным переменным:
// Самый частый пример использования — проход по ассоциативному массиву:
for (const auto& [key, value] : map) {
std::cout << key << ": " << value << std::endl;
}
Под кортежеподобным объектом я буду подразумевать такой объект, для которого известно количество доступных внутренних элементов на момент компиляции (от «кортеж» — упорядоченный список с фиксированным количеством элементов (вектор)).
Под это определение попадают такие типы, как:
std::pair
, std::tuple
, std::array
, массивы вида «T a[N]
», а также различные самописные структуры и классы.Стоп… В структурном связывании можно использовать свои собственные структуры? Спойлер: можно (правда, иногда придется поднапрячься (но об этом ниже)).
Как оно работает
Работа структурного связывания заслуживает отдельной статьи, но, раз мы говорим именно о «скользких» местах, я постараюсь кратко пояснить, как все устроено.
В стандарте дается следующий синтаксис для определения связывания:
attr(optional) cv-auto ref-operator(optional) [ identifier-list ] expression;
attr
— опциональный список атрибутов;
cv-auto
— auto с возможными модификаторами const/volatile;
ref-operator
— опциональный спецификатор ссылочности (& или &&);
identifier-list
— список имен новых переменных;
expression
— выражение, дающее в результате кортежеподобный объект, который используется для связывания (expression может быть в виде «= expr
», «{expr}
» или «(expr)
»).
Важно отметить, что количество имен в
identifier-list
должно совпадать с количеством элементов в объекте, получаемом в результате выполнения expression
.Это все позволяет писать конструкции вида:
const volatile auto && [a,b,c] = Foo{};
И тут мы попадем на первое «скользкое» место: встречая выражение вида «
auto a = expr;
», привычно подразумеваешь, что тип «a
» будет вычислен по выражению «expr
», и ожидаешь, что в выражении «const auto& [a,b,c] = expr;
» будет сделано то же самое, только типы для «a,b,c
» будут соответствующими const&
типами элементов «expr
»... Истина же отличается: спецификатор «
cv-auto ref-operator
» используется для вычисления типа невидимой переменной, в которую присваивается результат вычисления expr (то есть компилятор заменяет «const auto& [a,b,c] = expr
» на «const auto& e = expr
»).Таким образом появляется новая невидимая сущность (здесь и далее буду называть ее {e} ), впрочем, сущность весьма полезная: например, она может материализовывать временные объекты (поэтому можно спокойно их связывать «
const auto& [a,b,c] = Foo {};
»). Второе «скользкое» место вытекает сразу же из замены, которую делает компилятор: если тип, выведенный для {e}, не является ссылочным, то результат
expr
будет скопирован в {e}.Какие же типы будут у переменных в
identifier-list
? Начнем с того, что это будут не совсем переменные. Да, они ведут себя как самые настоящие, обычные переменные, но только с тем отличием, что внутри они ссылаются на связанную с ними сущность, причем decltype
от такой «ссылочной» переменной будет выдавать тип именно сущности, на которую эта переменная ссылается:std::tuple<int, float> t(1, 2.f);
auto& [a, b] = t; // decltype(a) — int, decltype(b) — float
++a; // изменяет, как «по ссылке», первый элемент t
std::cout << std::get<0>(t); // выведет 2
Сами же типы определяются следующим образом:
- Если {e} — массив (
T a[N]
), то тип будет один — T, cv-модификаторы будут совпадать с таковыми у массива.
- Если {e} имеет тип E и поддерживает интерфейс кортежей — определены структуры:
std::tuple_size<E>
std::tuple_element<i, E>
и функция:
get<i>({e}); // или {e}.get<i>()
то тип каждой переменной будет типомstd::tuple_element_t<i, E>
- В иных случаях тип переменной будет соответствовать типу элемента структуры, к которой выполняется привязка.
Итак, если совсем кратко, при структурном связывании выполняются следующие шаги:
- Вычисление типа и инициализация невидимой сущности {e} исходя из типа
expr
иcv-ref
модификаторов.
- Создание псевдопеременных и привязка их к элементам {e}.
Структурное связывание своих классов/структур
Главное препятствие к связыванию своих структур — отсутствие в C++ рефлексии. Даже компилятору, который, казалось бы, должен уж точно знать о том, как устроена внутри та или иная структура, приходится несладко: модификаторы доступа (public/private/protected) и наследование сильно затрудняют дело.
Из-за подобных трудностей ограничения на использование своих классов весьма жесткие (по крайней мере пока: P1061, P1096):
- Все внутренние нестатические поля класса должны быть из одного базового класса, и они должны быть доступны на момент использования.
- Или класс должен реализовать «рефлексию» (поддержать интерфейс кортежей).
// Примеры «простых» классов
struct A { int a; };
struct B : A {};
struct C : A { int c; };
class D { int d; };
auto [a] = A{}; // работает (a -> A::a)
auto [a] = B{}; // работает (a -> B::A::a)
auto [a, c] = C{}; // ошибка: a и c из разных классов
auto [d] = D{}; // ошибка: d — private
void D::foo()
{
auto [d] = *this; // работает (d доступен внутри класса)
}
Реализация интерфейса кортежей позволяет использовать любые свои классы для связывания, однако выглядит чуть громоздкой и таит в себе еще один «подводный камень». Давайте сразу на примере:
// Небольшой класс, который должен возвращать ссылку на int при связывании
class Foo;
template<>
struct std::tuple_size<Foo> : std::integral_constant<std::size_t, 1> {};
template<>
struct std::tuple_element<0, Foo>
{
using type = int&;
};
class Foo
{
public:
template<std::size_t i>
std::tuple_element_t<i, Foo> const& get() const;
template<std::size_t i>
std::tuple_element_t<i, Foo> & get();
private:
int _foo = 0;
int& _bar = _foo;
};
template<>
std::tuple_element_t<0, Foo> const& Foo::get<0>() const
{
return _bar;
}
template<>
std::tuple_element_t<0, Foo> & Foo::get<0>()
{
return _bar;
}
Теперь «привязываем»:
Foo foo;
const auto& [f1] = foo;
const auto [f2] = foo;
auto& [f3] = foo;
auto [f4] = foo;
И самое время подумать, какие типы у нас получились? (Кто смог сразу ответить правильно, заслуживает вкусную конфетку.)
decltype(f1);
decltype(f2);
decltype(f3);
decltype(f4);
Правильный ответ
decltype(f1); // int&
decltype(f2); // int&
decltype(f3); // int&
decltype(f4); // int&
++f1; // это сработает и поменяет foo._foo, хотя {e} должен был быть const
Почему так получилось? Ответ кроется в специализации по умолчанию для
std::tuple_element
:template<std::size_t i, class T>
struct std::tuple_element<i, const T>
{
using type = std::add_const_t<std::tuple_element_t<i, T>>;
};
std::add_const
не добавляет const
к ссылочным типам, поэтому и тип для Foo
будет всегда int&
.Как это победить? Просто добавить специализацию для
const Foo
:template<>
struct std::tuple_element<0, const Foo>
{
using type = const int&;
};
Тогда все типы будут ожидаемыми:
decltype(f1); // const int&
decltype(f2); // const int&
decltype(f3); // int&
decltype(f4); // int&
++f1; // это уже не сработает
Кстати, это же поведение справедливо и для, например,
std::tuple<T&>
— можно получить неконстантную ссылку на внутренний элемент, даже несмотря на то, что сам объект будет константным.
О чем нужно помнить
- «
cv-auto ref
» в «cv-auto ref [a1..an] = expr
» относится к невидимой переменной {e}.
- Если выведенный тип {e} не является ссылочным, {e} будет инициализирована копированием (осторожно с «тяжеловесными» классами).
- Связанные переменные — «неявные» ссылки (они ведут себя как ссылки, хотя
decltype
возвращает для них нессылочный тип (кроме тех случаев, когда переменная ссылается на ссылку)).
- Нужно быть внимательными при использовании ссылочных типов для связывания.
Оптимизация возвращаемого значения (rvo, copy elision)
Пожалуй, это была одна из самых бурно обсуждаемых фичей стандарта C++17 (по крайней мере, в моем кругу общения). И действительно: C++11 принес семантику перемещения, которая сильно упростила передачу «внутренностей» объекта и создание различных фабрик, а C++17 вообще, казалось бы, дал возможность не задумываться о том, как возвращать объект из какого-нибудь фабричного метода, — теперь все должно быть без копирования и вообще, «скоро и на Марсе все зацветет»…
Но давайте будем немного реалистами: оптимизация возвращаемого значения — не самая простая для реализации штука. Очень рекомендую посмотреть вот это выступление с cppcon2018: Arthur O'Dwyer «Return Value Optimization: Harder Than It Looks», в котором автор рассказывает, почему это сложно.
Краткий спойлер:
Есть такое понятие, как «слот для возвращаемого значения». Этот слот — по сути, просто место на стеке, которое выделяет тот, кто вызывает, и передает вызываемому. Если вызываемый код точно знает, какой единственный объект будет возвращен, он может просто сразу, напрямую создать его в этом слоте (при условии, что размер и тип объекта и слота совпадают).
Что из этого следует? Давайте сразу разбирать на примерах.
Здесь все будет хорошо — сработает NRVO, объект сконструируется сразу в «слоте»:
Base foo1()
{
Base a;
return a;
}
Здесь уже нельзя однозначно определить, какой объект должен быть в итоге, поэтому будет неявно вызван move-конструктор (c++11):
Base foo2(bool c)
{
Base a,b;
if (c) {
return a;
}
return b;
}
Здесь чуточку сложнее… Так как тип возвращаемого значения отличается от объявленного типа, неявно
move
вызвать нельзя, поэтому по умолчанию вызовется copy-конструктор. Чтобы этого не произошло, нужно явно вызвать move
:Base foo3(bool c)
{
Derived a,b;
if (c) {
return std::move(a);
}
return std::move(b);
}
Казалось бы, это — то же самое, что и
foo2
, но тернарный оператор — весьма своеобразная штука…Base foo4(bool c)
{
Base a, b;
return std::move(c ? a : b);
}
Аналогично
foo4
, но еще и тип другой, поэтому move
нужен точно:Base foo5(bool c)
{
Derived a, b;
return std::move(c ? a : b);
}
Как видно из примеров, над тем, как возвращать значение даже в, казалось бы, тривиальных случаях, все еще приходится задумываться… Есть ли способы немного упростить себе жизнь? Есть: clang с некоторых пор поддерживает диагностику необходимости явного вызова
move
, да и существует несколько предложений (P1155, P0527) в новый стандарт, которые сделают явный move
менее нужным.О чем нужно помнить
- RVO/NRVO сработает только в том случае, если:
- однозначно известно, какой единственный объект должен быть создан в «слоте возвращаемого значения»;
- типы возвращаемого объекта и функции совпадают.
- Если есть неоднозначность в возвращаемом значении, то:
- если типы возвращаемого объекта и функции совпадают — move будет вызван неявно;
- иначе — надо явно вызвать move.
- Осторожно с тернарным оператором: он краток, но может потребовать явный move.
- Лучше использовать компиляторы с полезными диагностиками (или хотя бы статические анализаторы).
Заключение
И все-таки я люблю C++ ;)
RH215
В последнее время понял одну замечательную вещь: я стал уставать от C++. Слишком много неявных вещей, слишком много слов, слишком много борьбы с языком.
Alexey_Alive
Попробуй новые языки. Rust, например, предлагает безопасность намного выше, чем C++, не имеет тонны Легаси, а также не фанатичны к выборы парадигмы. D сейчас есть в варианте без сборщика мусора. А если нужно что-то простое, так ещё и "поближе" к железу, то есть старый добрый C с расширениями GCC. Там и автовывод типов добавили, и безопасные макросы, и дженерики, и даже RAII.
RH215
Так как раз после Rust'а как раз C++ и воспринимается грустнее: много похожих на совеременный C++ идей, нет legacy, выглядит аккуратнее и без килотонн подводных камней.
А D язык хороший, но «не взлетел», к сожалению.
yaroslavche
aaprelev
Alexey_Alive, направьте меня в сторону автовывода типов? Не нашел среди C Extensions в документации GCC. RAII — это вы об атрибуте
__cleanup__
?Alexey_Alive
Да, я про cleanup. Это некий аналог RAII, ибо в Си нет классов. По поводу вывода типов, в Gcc есть аналоги auto и decltype из cpp: __auto_type и typeof. Благодаря им можно писать безопасные макросы, например (в GCC в ({ }) последнее выражение возвращается.)
aaprelev
Спасибо. Про
__cleanup__
знал, про__auto_type
— нетKanuTaH
Ну не знаю, у меня нет какого-то ощущения «борьбы с языком». Появляются новые фичи, которые ты можешь использовать (предварительно разобравшись, как они работают), а можешь не использовать, это ж никто не заставляет.
Sazonov
Поддерживаю. Но самое трудное — согласовать свои желания с коллегами. Чтобы не получилось ситуации, что один использует с++03, а другой фигачит лямбды с вариадиками.
KanuTaH
Ну, для этого я и знакомлюсь с новыми стандартами, чтобы худо-бедно разбираться в «лямбдах с вариадиками» :) Так-то в принципе я и сам использую те же лямбды, constexpr, кортежи, считаю это удобными механизмами. Другое дело что всякие SFINAE кунштюки например я использую редко, это скорее для разработчиков библиотек, они, так сказать, страдают за нас, чтобы нам было проще и естественней использовать их библиотечные интерфейсы :)
dipsy
Вот да, надо просто пользоваться языком, где всё явно, мало слов, нет особой борьбы с языком и можно кратко и выразительно оформить нужные алгоритмы в код, компилирующийся в быстрый исполняемый файл. Как там его, язык этот, не напомните название?
KanuTaH
У Эллочки Людоедки был такой. Только названия у него нет — сэкономила одно слово в своем словаре.
DoctorRoza
Ну тогда Lisp, как Дядька Боб глаголит
0xd34df00d
Да это ж хаскель!
Whuthering
Неужели Go?
mapron
Вариантов выше много — неужто Pascal?)
NBAH79
разбить программу на два слоя: оптимизированный на с++, и логику на шарпе
alan008
Delphi/Pascal конечно же
IGR2014
Assembler?
PeterK
Я от С++ ушел около 3-х лет назад и ни разу не жалел. Статьи, подобные этой, укреплают меня в моем решении: никогда. В языке даже синглтон толком нельзя реализавать без утечки памяти. «Структурное связывание».
0xd34df00d
Это почему ещё?
PeterK
Неясно, когда его создавать/удалять. С созданием еще можно разобраться, а с удалением — труба…
KanuTaH
А? Классика же:
Что касается «с удалением — труба», так это тоже просто от плохого знания предмета:
Я всегда замечал, что наиболее ярые критики C++, которые «ни за что и никогда», просто плохо знакомы с языком.
PeterK
Теперь возьмем конкретный пример, где используется синглетон почти универсально: логгер. И сколь-нибудь нетривиальную программу, где помимо локальных переменных/объектов на стеке всегда будет код, который выполняется после завершения main() и становится ясно, что этот логгер может уже и не существовать, когда он нам понадобится.
KanuTaH
Ссылочку ниже прочли? :) Там и рецептик приводится. Понятно, что чуть сложнее, чем мой пример выше, и его тоже можно при очень сильном желании поломать, но тем не менее.
AllexIn
Конечно ярык критики С++ это те кто его не осилил.
Так в этом и состоит суть критики: сложно осилить.
Я каждый день пишу на С++ и только на С++.
И я его сегодня знаю хуже чем лет пять назад.
Работа комитета вызывает больше негатива, чем позитива.
Да, язык нужно развивать. Но комитет ударился в впихивание всего подряд. Половину нового можно выкинуть, потому что оно является синтаксическим сахаром, мало нужным в повседневной работе. По сути просто пытаются один язык превратить в другой. На выходе получается гребаный франкенштейн. Не удивлюсь если через пару лет в стандарте внезапно появится GC. Не, ну а чо, полезная же штука!
KanuTaH
Ну, я не согласен. Например, spaceship operator — это синтаксический сахар? Да. Нужно ли выкинуть? ИМХО нет, потому что он избавляет от написания просто тонны boilerplate кода. Да, конечно, можно писать "по старинке", педалить все вот эти operator ==, !=, <, >, а ещё про friend operator не забыть, все по новой… Но зачем? В чем профит? А "ниасилить" тоже можно по-разному, можно не разбираться в тонкостях SFINAE, а можно быть неспособным реализовать синглтон без утечки памяти или испытывать сакральную боязнь object slicing'а, потому что где-то прочёл, что это "плохо", а почему именно это плохо и в каких случаях — не понимаешь, а ведь эти вещи ещё из C++98. В общем, незнание незнанию рознь.
AllexIn
Я не говорю, что всё что делает комитет хрень.
Но вот тот же структурный биндинг из статьи — нафиг не нужен. Взять из переменной набор, вместо того чтобы обращаться к полям через переменную, что не составляет труда. К тому же тот же with из delphi гораздо более адекватное и красивое решение, если уж настолько критично не писать название переменной для доступа к члену…
Вообще, ИМХО, комитету не хватает некой дополнительной «проверки временем». То есть добавили фичу, если через 5 лет этой фичей не пользуются большинство крупных игроков на С++ рынке — она выносится еще раз на обсуждение и если весомых доводов её оставить нет — depricated и досвидания в следующей редакции.
KanuTaH
Ну это я так понимаю скорее для всяких кортежей, особенно если в них ссылки. Вместо написания простынки из создания временных tuple и std::get.
qw1
AllexIn
Значит более жестко подходить на этапе перехода от экспериментальной ветки в стабильную.
Чтобы не было ситуаций, когда «Так, сегодня мы утверждаем переход в релиз фичи Х. Кстати, кто уже делал проекты с её использованием?.. Кто хотя бы пробовал?.. Ясно, переносим обсуждение на следующую встречу».
Напомню, примерно так было с одной из фич на последней встрече.
Засрать язык фичами, которые использует два с половиной фаната — это тоже верный способ убить язык.
qw1
Тут я согласен: новое нужно вводить с большой осторожностью. Но отзывать уже принятое — нельзя.
Нам, как пользователям стандарта, какое должно быть дело до обсуждаемого. Приняли — придётся с этим жить. Пока не приняли — даже и смотреть незачем, когда оно ещё будет принято…
Antervis
ну примерно так и происходит, только не 5 лет, а 2-3 года. Или вы никогда не пытались пользоваться бустом/experimental?
какие фичи с++17, по-вашему, используют «два с половиной фаната»? С++14? С++20?
AllexIn
Что там насчет «Garbage collector support»? :))
А если серьезно вопрос достаточно сложный. Я на него не могу твердо ответить.
На любой мой ответ можно будет возразить: «Вот код, где это используется». Статистики то у меня нет, только ощущение от работы в разных командах плюсовиков.
Antervis
Это с++11, там да, есть несколько редко используемых фич. Собственно, после него в методологии развития стандарта многое поменялось.
ну вы назовите фичи, которые, на ваш взгляд, почти никто не использует. Ваше мнение же должно быть основано на конкретных примерах?
AllexIn
Какое мнение? Я нигде не делал утверждение, что сейчас в стандарте есть фичи которые используется два калеки. Делать такое утверждение — большая ответственность. Надо быть большим экспертом. принимающим активное участие в анализе кодовой базы чтобы такое утверждение сделать.
Я сказал что делать такие фичи — ошибка. Обозначил тенденцию.
0xd34df00d
Писать
тупо удобнее, чем
Или там, не знаю,
Да, это сахар, но это очень приятный и удобный сахар, которым я достаточно регулярно пользуюсь.
Ryppka
Но, как и написано в статье, понимание того, что при этом будет создано, требует знаний и усилий. Не скажу, что критично, но осадочек остается: например, не будет ли значимых различий между компиляторами? Между версиями одного и того же компилятора?
0xd34df00d
На получение знаний нужно потратить время один раз, да, но потом вы ими будете пользоваться
всю жизньдолго.Нет, стандарт же вполне однозначно всё определяет.
Ryppka
О да, стандарт сила, кто бы возражал. Вспомните, к примеру, так прекрасно разобранную О'Двайром историю про gcc, clang, msvc и разные форматы декорирования имен.
Что касается структурного связывания, то у меня не хватает ума, чтобы без просмотра сгенерированного ассемблера наверняка понять, что и как создается.
Antervis
у меня для вас новости из 11-ого года
AllexIn
Счетчики ссылок(чем являются смартпоинтеры) и GC — сильно разные вещи.
Смартпоинтеры — логичное развитие голых указателей. По сути голые указатели не должны вообще хранится где-либо, кроме участка кода, который с ними непосредственно сейчас работает. Тут ни оверхеда по производительности нет, ни непредсказуемости.
А вот GC — совсем другая история. Но, повторюсь, все таки верю в светлое и надеюсь до имплементации GC в стандарте дело не дойдет.
Antervis
я не про смартпоинтеры. По ссылке выше есть набор функций из категории «Garbage collector support», и он присутствует в стандарте языка с++. Другое дело что стандарт даже не требует от компилятора поддержки этого функционала чтобы считаться полностью соответствующим стандарту, разрабы компиляторов этот функционал не делают, а народу попросту пофиг.
AllexIn
Это сделано для того, чтобы те, кому нужен GC могли его относительно легко реализовать.
0xd34df00d
Например?
Я в повседневной работе использую и folding expressions, и if constexpr, и structured bindings, и буду использовать кучу всякой ерунды из C++20 вроде того же уже упомянутого spaceship operator.
KanuTaH
Разверните эту свою мысль. А то есть мнение, что вы до C++ так и не дошли, так сказать, перед тем, как от него уходить.
qw1
Возможно, речь о недавней статье habr.com/en/post/455848
KanuTaH
Ну не знаю, там речь все-таки не об утечке (деструктор-то у синглтона вызывается исправно, хехе), а о неопределенном поведении, вызванном конкретными «диверсионными» действиями со стороны пользователя библиотеки (понятно, что по незнанию). Случай интересный, но к «утечкам памяти» от синглтонов как таковых он все-таки отношения не имеет.
mayorovp
Синглтон по определению не может вызвать утечку памяти. Просто потому что для утечки требуется бесконтрольное выделение памяти под всё новые объекты, а синглтон всегда один.
KanuTaH
Видимо имеется в виду, что «если я создам синглтон через new, то некому будет вызвать delete». Что само по себе правильно, конечно, но синглтоны создаются не так.
Whuthering
Если у синглтона приватный конструктор, то его по-хорошему нельзя будет создать через new.
KanuTaH
Можно, если создавать из статического метода этого же класса.
Whuthering
Ну да, а можно еще и friend-классами обмазаться. Поэтому я и написал «по-хорошему», а если человек сам осознанно собирается отстрелить себе ногу, то понятное дело что C++ ему мешать в этом не будет :)
Ryppka
Бывало в юные года…
Писал на «сплюсплюс» тогда…
Теперь угас уж жар в крови:
На «Си» пишу, на «чистом С»…
P.S.
Статья хорошая, понравилась.
WRP
Вот и я о том же…
Знаю С, но как понять синтаксис нового C++ для меня загадка.
sborisov
Си любят за то, что там таких подводных камней нет (есть другие наверное), к примеру — там всегда есть явное выделение памяти и удаление. То есть известно всегда какие инструкции будут выполнены, а не предположения о том какой конструктор будет вызван или не вызван, причём поведение это в С++ меняется от версии к версии (copy ellision).
Вот статья которая очень хорошо описывает подобную ситуацию с С++ (инициализация)
habr.com/ru/post/438492
Raynk
У меня складывается ощущение, что из C++ пытаются сделать современный язык путем натягивания совы на глобус. Где нужна скорость, использую чистый C. Где надо быстро что-то сделать, использую C#. Кто для каких целей использует C++ с последними плюшками?
RH215
C++ всё ещё удобнее в большинстве случаев. В С слишком много нужно делать руками.
soniq
Мне кажется, что вот эти трюки с явным-неявным управлением памятью требуют достаточно много когнитивных ресурсов. Когда пишешь на С — сосредоточен и внимателен ко всему, когда пишешь на С# — забиваешь на все эти детали. А С++ вроде сам за тебя много делает, но расслабляться нельзя.
alex_justes Автор
Сосредоточен и внимателен только до тех пор, пока код не становится достаточно большим и сложным, чтобы не помещаться целиком в когнитивные ресурсы...)
soniq
Перефразируя известное высказывание
alex_justes Автор
Сам по себе код может быть простым, однако всё усложняется, когда проект становится большим: код пишет сразу целая команда, связи между модулями, библиотеками, опять же легаси (которое или уже было или появляется со временем)…
Да даже свой собственный код через некоторое время становится сложным для понимания/вспоминания.
Так что я предпочитаю использовать инструменты, которые как-то автоматизируют процесс, руками, конечно, хорошо, но, через некоторое время, это становится слишком «дорого».
Ryppka
Это общая проблема для всех языков: есть границы масштабирования при росте размера и сложности. В каждом языке есть свои средства для выражения абстракций, структурирования кода и т.д., но граница, за которой их начинает не хватать все равно есть. У C++, имхо, эта граница дальше, чем у C, но не сказать, что уж сильно дальше.
Решить проблему можно только «внеязыковыми» средствами: архитектурой, продуманным дизайном и т.д. И вот тут есть интересный момент: можно добавлять в язык выразительные средства, а можно «опрощать» язык, чтобы уменьшать, так сказать, удельное логическое сопротивление на строчку кода. Получается, что кода-то очень много, но он «простой», в нем даже IDE может разобраться и построить схемы, навигацию и т.д. C — он изначально достаточно прост. А, к примеру, Java и Go сознательно сделаны такими.
0serg
В C++ просто надо явно объяснять компилятору то, чего ты хочешь получить на выходе. Если понимать этот «метаязык» то проблем вообще никаких. Причем там же буквально несколько простых принципов, не сказать даже что что-то сложное. Особенно ярко это видно если сравнивать современный C++ с тем что было до него. Там где раньше было «правило трех / правило пяти» сейчас работает «правило нуля» — компилятор при правильном его использовании все сам соберет верно, ничего самостоятельно переопределять вручную не нужно.
soniq
У вас же есть CI? Посмотрите там в логах, сколько раз за последнюю неделю юнит-тесты плюсового кода падали с сегфолтом, а сколько раз по ассерту. Потом расскажете, какой компилятор умный, и как он не даёт ошибаться.
0serg
Шутите? Это редчайшие события, как в тестах, так и в проде. В тестах хорошо если раз в год бывает сегфолт, в проде машин больше — там несколько случаев на сто тысяч запусков, но это в основном сбои железа, прежде всего памяти, они четко по машинам привязаны и при замене железа исчезают. Ассерты на логику да, могут вылетать но это с языком уже не связано.
0xd34df00d
Ну, на самом деле связано — чем больше проверок логики вы можете сделать в компилтайме, тем меньше ассертов у вас будет в рантайме (да и вообще тем меньше ассертов у вас будет).
soniq
Нет, это понятно что в мастере у вас все хорошо а на проде вообще идеально, вы посмотрите в каком состоянии код у разработчиков до того как они отправят его на ревью.
У нас тут распределённая сборка есть, так что можно посмотреть кто и что компилирует. На глаз, примерно 80% попыток вообще не компилируются. Я не поленился, и просмотрел последние 50 фейлов, когда проект собрался, юниты запустились но не прошли:
В 41 случае сработал ассерт в тесте
Ещё четыре падения когда сработал ассерт в продуктовом коде: попытались засунуть null в словарь, что-то не вызвали, и т.п.
Ну и пять сегфолтов родимых.
0serg
Ну, эти 80% не компилирующегося кода — это как раз то что «компилятор не пропустил» :). Это можно назвать недостатком языка, а можно — достоинством (скажем python который я нежно люблю в той же ситуации запустится, но затем помрет в рантайме что хуже). Падающие тесты — тоже хорошо. Нередко это говорит о том что они отлавливают достаточно много нетривиальных ситуаций, поэтому их и не получается пройти «с первой попытки» (а то видел я тесты которые внешне выглядели похоже на правду но на деле не фейлились даже на ошибочном коде, хех :)). Не думаю кстати что с тестами ситуация будет отличаться в других языках.
Но вот индексная арифметика — да, до коммита в прод может сегфолтить или ассертится, согласен. Разыменование null тоже бывает. Но это в любом языке будет ассертится, это ошибки в логике и не очень понятно при чем тут плюсы. А конкретно специфичную для плюсов работу с памятью в C++11 до сегфолта довести очень сложно.
soniq
Не анализировал, почему у них не собирается. Может в скобках запутались, или хедеров не хватило. А может быть действительно компилятор что-то обнаружил.
Абсолютно согласен, что юниты хорошая штука для любого языка, они помогают не держать в голове одновременно все требования. А если тесты хорошие, то даже можно обо всех требованиях и не знать — тесты подскажут что апи добавления комментов должно проверить не забанен ли пользователь, а алгоритм поиска пути обходит скалы и реки, и предпочитает дороги. Ну или что там продукт делает.
Я знаю имена как минимум пятерых, кто сегодня смог это сделать. Не думаю, что они к этому как-то специально стремились. Просто чуть ослабла концентрация, отвлёкся на задачу и привет.
0serg
Тогда Вы явно делаете что-то неправильно :)
soniq
То есть, существуют некие очень простые правила, которые можно вставить, например, в плагин к IDE, и они бы подсветили этим товарищам место где они делают что-то явно неправильное и вот эти микро-факапы бы не случились?
0serg
Умные указатели + грамотно спроектированное приложение. К сожалению не скажу что это тривиально автоматизируется, но научиться вполне можно :)
soniq
Получается, что компилятор С++ не может проверить корректность полностью, а значит за ним придётся доделывать нейронам в чьей-то голове. Автоматизация же, ну.
0serg
Корректность полностью проверить не может ни один язык кроме, возможно, функциональных :)
soniq
Ну простые вещи уже много где научились проверять, не без влияния функциональщиков конечно, но что поделаешь. Чтобы получить в 2019 году NullReferenceException, или тем более IndexOutOfRangeException надо специальный комментарий рядом написать, иначе кодоанализатор загнобит.
RH215
Особенно нельзя расслабляться, если в используемых тобой библиотеках управляют памятью вручную. :)
daiver19
Какие современные языки умеют RVO, например? Мне кажется, что философия С++ достаточно самобытна, чтоб говорить не о натягивании совы на глобус, а просто о планомерном развитии. C++11 пофиксил многие проблемы тяжелого наследия С (привет, ручное управление памятью), теперь просто добавляют всякий сахар/оптимизации, как и другие современные языки.
AllexIn
Добавляют новые способы реализовать то, что и так уже можно нормально(без извращений) реализовать. Это усложнение языка и ведет только к плохому.
DaylightIsBurning
например, что такого добавили, что можно было без извращений реализовывать раньше?
AllexIn
Знаете что такое boost?
Он весь реализован на базовом С++.
И он почти весь переехал в стандарт.
Кстати, далеко не самое плохое что внесли в стандарт.
alex_justes Автор
А вы заглядывали внутрь boost? Боюсь, если разобраться, как там внутри всё работает, то фильмы ужасов или самое извращенное порно могут начать казаться уже и не такими страшными…
0xd34df00d
Ну, во-первых, какого-нибудь фьюжона или ханы до сих пор нет.
Во-вторых, у меня есть опыт портирования собственной библиотеки, довольно серьёзно обвешанной всяким метапрограммированием, с C++14 на C++17. И C++17, поверьте, очень сильно повышает удобство и качество жизни меня как реализатора библиотеки.
DaylightIsBurning
с кучей макросни и такого когда, на который смотреть больно.
Так что вопрос открыт: что такого добавили, что можно было без извращений реализовывать раньше?
Antervis
rust должен уметь — бекенд там llvm-ный, а RVO (насколько я в курсе) корректен для любого типа. Разве что если фронтенд такую оптимизацию прокидывать не умеет
0serg
С++ остается идеальным языком для CAD-систем, к примеру. Или игр. Вообще для любых приложений сколь-либо крупного размера где важно быстродействие. И он очень удобен если его правильно уметь использовать. Емкий, выразительный и быстрый код. Взять тот же Eigen к примеру. Он очень C++-style, в любом другом языке по-моему его аналоги просто невозможны. Он дает понятный читаемый код. И при этом он (в моих тестах) весьма заметно опережал «c-style» MKL, который был еще и менее читаемым. Хотя вот казалось бы.
Я резко не соглашусь с тем что C++ не является «современным языком». C++11 был радикальным шагом вперед, язык стал намного удобнее в использовании, более читабельным, более производительным. Ушла необходимость бороться с языком во многих местах. И сейчас C++17 — тоже большой шаг вперед. Не такой драматичный как C++11, но очень заметно упрощающий жизнь и убирающий потребность в некоторых критичные велосипедах.
mikeus
В примерах начиная с
(если здесь предполагается что Derived это производный класс от Base) происходит object slicing. Заботиться при этом о реализации move semantics как-то уже излишне.KanuTaH
В самом по себе object slicing нет ничего «незаконного», это просто механизм, у которого есть вполне легитимные применения. Почему бы и не озаботиться move semantics, если нужно?
alex_justes Автор
Как уже правильно заметили — это вполне себе рабочая практика. К тому же move — это, по сути, просто способ передать владение каким либо ресурсом без особых накладных расходов, и таким ресурсом как раз может быть что-нибудь общее, что как-то особенно считается в наследнике, например.
0serg
По-моему Вы описываете очень экзотическую реализацию того что проще и лучше реализовывать паттерном Factory
alex_justes Автор
Фабрики бывают разными. Не скажу, что slicing — это хороший метод для реализаций чего-нибудь, но это используемый метод. Один из примеров вполне используемого кода (некоторая обёртка над сырыми указателями) есть в презентации из статьи (CppCon 2018: Arthur O'Dwyer “Return Value Optimization: Harder Than It Looks”)
mikeus
Просто срезка режет глаз. Но в контексте темы copy elision это наверно, да, может служить иллюстрацией, что вот так вот, можно вызвать move-конструктор от некоторой части объекта.
WRP
C++ новейший настолько сильно отличается от C и «C с классами», что просто дрожь берёт.
Есть хоть какая-нибудь литература, чтобы попытаться сделать шаг через пропасть?
KanuTaH
Да, есть. Погуглите "C++ Core Guidelines".
WRP
Спасибо!
Или упаду в пропасть или пойму.
Вариантов два)
MSerhiy
Мне нравится как не с++ программисты обсуждают с++)
WRP
Вся вакханалия началась с STL, потом появился boost и прочее.
То есть по сути библиотеки и шаблоны создали новый язык программирования.
В итоге код на современном C++ ну явно нечеловеческий.
Он нечитаем.
100500 минусов в мою несуществующую карму, но это так.
DaylightIsBurning
Это всё появилось не от хорошей жизни, без него было ещё хуже.
WRP
Что значит «не от хорошей жизни»?
sborisov
Вот прекрасная статья от «отцов основателей», почему STL именно такая и какие задачи она решала.
habr.com/ru/post/166849
ss-nopol
С++ уверенно движется по пути когда-то намеченным перлом. Если всё так продолжится то известную картинку можно будет переделывать под C++.