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

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

Полностью согласен.

О да, давайте вставим тысячи бесполезных вызовов memset там где они нахрен не нужны. Потрясающий пропозал (нет)

а разве недостаточно что в компиляторах (gcc/clang) есть флаг -ftrivial-auto-var-init=zero?

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

Ой, Вы не устали гвозди в крышку гроба С++ забивать? Хоть почитайте о чем в том пропозале речь идет. Или не читал, но осуждаю?..

std::string s = "meow";

Обратим внимание, что в данном случае символ = не является оператором присваивания.

А почему он не является оператором присваивания?

Вроде как std::string s объявление переменной - выделение памяти для переменной,

= "meow" копирование данных в эту память, по сути присваивание значения переменной,

разве не так?

Присваивание применяется к ранее созданным объектам и поэтому включает обязательный этап — освобождение ресурсов этого объекта. Конечно, для простых переменных типа int ничего освобождать не надо, но для более сложных, то том числе и для std::string, это делать обязательно. А вот при инициализации этого делать не нужно, так как мы инициализируем пустой объект, то есть инициализация более простая процедура. При инициализации работает конструктор объекта а при присваивании оператор присваивания, это разные сущности (хотя и связанные).

При инициализации работает конструктор объекта

вы хотите сказать что эти два варианта:

std::string s = "meow";

std::string s("meow");

эквивалентны? В обоих случаях будет вызван конструктор?

в вашем примере с ассемблером я вижу 4 (четыре) вызова конструкторов стринга,

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

вряд ли это что-то проясняет.

А вы промотайте немного дальше горизонтальный скролл справа, и увидите, что конструктора там всего два, и в оба передается строка. Другие два вызова - вызовы оставшихся от деструкторов _M_dispose().

вызовы оставшихся от деструкторов _M_dispose().

да-а, слона-то я и не приметил :), забыл думать про деструкторы, последнее время.

Действительно '=' - это вызов конструктора с одним параметром в этом случае.

Да.

И еще

std::string s{ "meow" };

std::string s = { "meow" };

auto s = std::string("meow");

auto s = std::string{ "meow" };

Все эти варианты вызывают один и тот же конструктор.

С точки зрения семантики это разные операции, но сточки зрения оптимизатора одно и то же. Если вы удалите копирующий конструктор у типа, то компиляция завершится ошибкой, иначе же оптимизатор исключит ненужные вызовы. К этому же относится конструкции вида Т(Т(Т(Т(Т())))), это всё вызовет лишь один конструктор.

Ну, а что бы отключить оптимизатор надо везде лепить volatile))

Эта два варианта не совсем эквивалентны (хотя в последних стандартах языка они почти эквивалентными). Есть, однако, нюансы.

Однако в остальном - именно так. Это именно вызов конструктора в обоих случаях.

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

А вы уверены в этом? Точнее, вы уверены, что "присвоение" связно с "освобождением"?
Может быть вы имели ввиду "создание" и "освобождение", т.к. работу конструктора и декструткора объектов?

Вот как можно описать семантику присваивания


X& X::operator=X(const X& src)

{

if (this != std::addressof(src))

{

this->~X();

new (this)(src);

}

return *this;

}


Правда этот код является антипаттерном, то есть это будет работать, но имеет потенциальные проблемы. Подробнее в разделе 3.9 в моей статье про перегрузку операторов https://habr.com/ru/articles/489666/.

Да, вы правильно поняли, я именно это и имел ввиду. Как правило при операции присвоения сперва вызывается деструктор старого объекта, а потом в переменную записывается другое значение, полученное с помощью конструктора нового объекта.

Вот только формально вызов деструктора (т.е. "освобождение"), не относится к операции "присвоения", так как может выполнятся и без этой операции.

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

Операция присваивания в пользовательском классе, это функция класса, а не "оператор" присвоения. Оператор присвоения использует эту функцию, но она сама не является частью "оператора присвоения".

Я хочу сказать, что связь тут обратная. Операция присвоение использует то, что необходимо в каждом конкретном случае (который зависит от типа данных), и для объектов будет вызываться "освобождение". Но это не часть оператора присвоения, а часть объекта, который создается или уничтожается. И если объект не требует "освобождения" ресурсов, то это и не делается.

А в чем проблема реализовать оператор присвоения без явного вызова деструктора и получения каких-то утечек? Более того можно использовать swap-идиому для реализации оператора присвоения и, снова же, без явного вызова деструктора.

Да, стандартный правильный паттерн реализации присваивания использует идиому копирование-обмен. Сначала создается временный объект с новым состоянием, потом происходит обмен состояний этого временного объекта с исходным объектом и после этого неявно вызывается деструктор для временного объекта, который уже будет содержать состояние исходного объекта. Об этом подробно написано в упомянутой статье про перегрузку операторов.

Copy-and-swap - известный паттерн реализации присваивания, но "стандартным" его называть странно. У этого паттерна много недостатков, из-за чего его можно применять только сознательно рассмотрев все "за" и "против". То есть использовать его в качестве паттерна "по умолчанию" ни в коем случае нельзя. Это, по-моему, уже исключает использование слова "стандартный".

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

Во-первых, потому что в стандарте так сказано.

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

У, я вам проклятого принёс.

int typedef a;
long thread_local unsigned extern long d;

int typedef a;
Это то же самое, что
typedef int b;
Последний вариант это классический синтаксис. Первый вариант это некие допустимые вариации, которых в C++ довольно много и их лучше не использовать, чтобы не запутать себя и потенциальных читателей кода.

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

Некоторые встроенные типы в С++ используют несколько ключевых слов. Порядок этих слов стандартом не фиксирован и поэтому следующие три объявления объявляют переменные одного и того же типа.
unsigned long long d1;
long unsigned long d2;
long long unsigned d3;

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

thread_local и extern можно сочетать в одном объявлении (компилятор и компоновщик не ругаются). Судя по всему (по тултипу и отсутствие ошибок компоновщика) extern в данном случае просто игнорируется. Увы, в C++ довольно много подобных малопонятных с первого взгляда конструкций и их следует избегать. Как писал Герб Саттер не суйтесь в темные закоулку C++, пишите код в котором вы полностью уверены.

Судя по всему (по тултипу и отсутствие ошибок компоновщика) extern в данном случае просто игнорируется.

Да неужели.

Увы, в C++ довольно много подобных малопонятных с первого взгляда конструкций и их следует избегать.

Ничего малопонятного, а тем более "неработающего" тут нет.

Да, виноват, немного поспешил, extern не игнорируется. Даже есть гипотеза, как это работает, но это надо проверять. Сам бы я никогда не написал бы подобный код и тратить время на его исследование особого желания нет.

На днях меня очень не порадовало, что structured binding это не объявление переменной. Оно, вроде понятно, но то, что их нельзя захватывать в замыканиях, очень не удобно.

12:27:31 @mac:/tmp$ clang++ -std=c++20 test.cpp
12:27:34 @mac:/tmp$ ./a.out
a: 1, b: 2
12:27:35 @mac:/tmp$ cat test.cpp
#include <iostream>
#include <utility>

std::pair<int, int> foo() {
    return std::make_pair(1, 2);
}

template<typename T>
void bar(T t) {
    t();
}
int main() {
    const auto [a, b] = foo();

    auto fu = [&](){
        std::cout << "a: " << a << ", b: " << b << std::endl;
    };

    bar(fu);
}
12:27:40 @mac:/tmp$

Что значит нельзя захватить в замыканиях? Нормально захватываются

Собственно с 20 стандарта таки разрешили захватывать. В 17 стандарте, в котором они появились это видимо не работало в некоторых случаях.

В MSVS2019 при использовании c++17 компилятор не позволяет такое захватывать.

Надо с c++20 проверить.

Там по ссылке в описании как раз про захват рассказано. По дефолту в 17 оно в tuple element превращалось при биндинге и семантика запрещала захват кроме каких-то отдельных случаев. В 20х это дело пофиксили.

Как правильно отметили выше, это работает начиная с С++20. До этого было компиляторозависимо, где-то даже работало, кажется в GCC. Но как это часто бывает, свежий стандарт в работе недоступен.

Насколько помню, static определяет место физического хранения переменной (т.е., перемещает переменную из сегмента стека в сегмент данных), а всё остальное - скорее побочка этого перемещения. Или что-то поменяли?

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

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

Объявление в С++ имеет не тот базовый вид, который приведен в статье, а вид

type cv-qual pr-specs name fa-specs, pr-specs name fa-specs, ..., pr-specs name fa-specs;

(идя на поводу у предложенного статьей формата).

То есть после общей части type cv-qual следуют множественные деклараторы pr-specs name fa-specs, разделенные запятыми. При этом ключевым моментом для правильного понимания семантики такого объявления является то, что часть type cv-qual является общей для всех и относится ко всем деклараторам. В отличие от части pr-specs, которая является индивидуальной для каждого декларатора.

Именно этот факт приводит к тому, что cv-квалификаторы в составе cv-qual ведут себя не так, как cv-квалификаторы в составе индивидуальных pr-specs. Первые относятся ко всем деклараторам в объявлении, а вторые - только к своему декларатору. Именно поэтому никаких аргументов в пользу синтаксиса со смещенным вправо const не существует и какая-то пропаганда такого синтаксиса никем всерьез не рассматривается. Cv-квалификаторы в составе cv-qual всегда смещаются влево именно для подчеркивания вышеописанной особенности структуры объявления.

Тот вариант объявления, который вы привели, фактически описан в раздела 4.2. Объявление нескольких переменных в одной инструкции. Правда я не стал приводить описание в общем виде, а описал ситуацию на «на пальцах», но с подробным примером. Все предыдущие описания относились к объявлению одной переменной.

Ваши аргументы за левое расположение cv-qual понятны, они базируются на правилах объявления нескольких переменных в одной инструкции. Но как раз эта возможность — объявлять несколько переменных в одной инструкции, — многими авторами относятся к нерекомендуемым. А если объявлять одну переменную в инструкции, то появляются дополнительные аргументы для правого расположения cv-qual. Именно такой точки зрения придерживаются авторы [VJG].

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

Спасибо, упустил этот момент. Подумаю, как скорректировать статью.

Поправил.

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

Публикации

Истории