Comments 48
Держу кулаки за P2723 Zero-initialize objects of automatic storage duration, надеюсь что а) его таки примут, и б) в стандарте он окажется раньше ишачьей пасхи.
Полностью согласен.
О да, давайте вставим тысячи бесполезных вызовов memset там где они нахрен не нужны. Потрясающий пропозал (нет)
а разве
a[x] = {}
не поможет?
а разве недостаточно что в компиляторах (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().
Да.
И еще
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++ довольно много подобных малопонятных с первого взгляда конструкций и их следует избегать.
Ничего малопонятного, а тем более "неработающего" тут нет.
На днях меня очень не порадовало, что 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 проверить.
Как правильно отметили выше, это работает начиная с С++20. До этого было компиляторозависимо, где-то даже работало, кажется в GCC. Но как это часто бывает, свежий стандарт в работе недоступен.
Насколько помню, static определяет место физического хранения переменной (т.е., перемещает переменную из сегмента стека в сегмент данных), а всё остальное - скорее побочка этого перемещения. Или что-то поменяли?
Пробежал глазами статью (честно признаюсь, что пока не разбирал детально), но так и не нашел одного из самых главных и самых важных для понимания фактов о структуре объявления.
Объявление в С++ имеет не тот базовый вид, который приведен в статье, а вид
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 может использоваться для конструктора с любым количеством аргументов, даже в конструкторе без аргументов.
Объявление и инициализация переменных в C++