Ваши аргументы за левое расположение cv-qual понятны, они базируются на правилах объявления нескольких переменных в одной инструкции. Но как раз эта возможность — объявлять несколько переменных в одной инструкции, — многими авторами относятся к нерекомендуемым. А если объявлять одну переменную в инструкции, то появляются дополнительные аргументы для правого расположения cv-qual. Именно такой точки зрения придерживаются авторы [VJG].
Тот вариант объявления, который вы привели, фактически описан в раздела 4.2. Объявление нескольких переменных в одной инструкции. Правда я не стал приводить описание в общем виде, а описал ситуацию на «на пальцах», но с подробным примером. Все предыдущие описания относились к объявлению одной переменной.
Если static применяется к локальной переменной, то тут вы правы. Но static может применяться к членам класса и к глобальным переменным, в этом случае смысл этого ключевого слова другой. Это кратко описано в разделе 5.2.
Да, виноват, немного поспешил, extern не игнорируется. Даже есть гипотеза, как это работает, но это надо проверять. Сам бы я никогда не написал бы подобный код и тратить время на его исследование особого желания нет.
thread_local и extern можно сочетать в одном объявлении (компилятор и компоновщик не ругаются). Судя по всему (по тултипу и отсутствие ошибок компоновщика) extern в данном случае просто игнорируется. Увы, в C++ довольно много подобных малопонятных с первого взгляда конструкций и их следует избегать. Как писал Герб Саттер не суйтесь в темные закоулку C++, пишите код в котором вы полностью уверены.
Некоторые встроенные типы в С++ используют несколько ключевых слов. Порядок этих слов стандартом не фиксирован и поэтому следующие три объявления объявляют переменные одного и того же типа. unsigned long long d1; long unsigned long d2; long long unsigned d3; Классический синтаксис — это первый вариант, он появляется в сообщениях компилятора и его следует придерживаться, чтобы не запутывать себя и потенциальных читателей кода.
Да, стандартный правильный паттерн реализации присваивания использует идиому копирование-обмен. Сначала создается временный объект с новым состоянием, потом происходит обмен состояний этого временного объекта с исходным объектом и после этого неявно вызывается деструктор для временного объекта, который уже будет содержать состояние исходного объекта. Об этом подробно написано в упомянутой статье про перегрузку операторов.
Для пользовательских классов программист должен сам реализовать присваивание (правда иногда можно доверить генерацию присваивания компилятору). Правильно реализованное присваивание использует деструктор для старого состояния и конструктор для нового, то есть это комплексная операция. Если не использовать деструктор, то мы получить утечку ресурсов. Так что говорить, что вызов деструктора, не относится к операции присвоения мне кажется не вполне корректно.
Правда этот код является антипаттерном, то есть это будет работать, но имеет потенциальные проблемы. Подробнее в разделе 3.9 в моей статье про перегрузку операторов https://habr.com/ru/articles/489666/.
int typedef a; Это то же самое, что typedef int b; Последний вариант это классический синтаксис. Первый вариант это некие допустимые вариации, которых в C++ довольно много и их лучше не использовать, чтобы не запутать себя и потенциальных читателей кода.
Присваивание применяется к ранее созданным объектам и поэтому включает обязательный этап — освобождение ресурсов этого объекта. Конечно, для простых переменных типа int ничего освобождать не надо, но для более сложных, то том числе и для std::string, это делать обязательно. А вот при инициализации этого делать не нужно, так как мы инициализируем пустой объект, то есть инициализация более простая процедура. При инициализации работает конструктор объекта а при присваивании оператор присваивания, это разные сущности (хотя и связанные).
Здравствуйте, Вызов ::operator new(size) — это вызов стандартной функции выделения памяти, которая в качестве параметра принимает требуемый размер. Эта фунция только выделяет память, ничего больше. Вызов ::new(size) не будет компилироваться, так как в этом случае синтаксически правильной формой должна быть ::new X(/* аргументы конструктора */), где X — это имя класса. В данном случае ::new — это оператор new, который не только выделяет память, но и вызывает конструктор, то есть инициализирует объект. С уважением. Дмитрий Пономарев.
Спасибо за комментарий, очень образное описание сути интерфейса. Многие языки программирования поддерживают интерфейсы на уровне языка (например C#, Java), но вот C++ нет. Приходится эмулировать интерфейсы с помощью чисто виртуальных функций, но при этом возникают достаточно много тонкостей и потенциальных проблем. В своей статье я как раз и попытался разобраться с этими проблемами.
Вообще надо различать вывод аргумента шаблона и вывод типа параметра функции. Они могут не совпадать, типа параметра функции может быть украшен спецификатором ссылки, const и другими модификаторами. В шаблонах функций аргумента шаблона нам доступен, но в лямбдах с auto он будет скрытым.
Одна из идей, которой руководствуются при проектировании ссылок – это сделать ссылку максимально неотличимой от объекта, на который она ссылается. Этим можно объяснить, почему при инициализации C++ ссылки не используется специальный оператор (типа & в случае указателей) и не используется оператор разыменования (типа * в случае указателей) для доступа к объекту. В результате возникают некоторые коллизии, но небольшие, например, нельзя перегрузить функции, у которых параметр передается по значению и по ссылке на константу. Теперь про неизменяемость C++ ссылок, то есть отсутствие нулевых ссылок (обязательная инициализация) и невозможность перенаправить на другую переменную. Посмотрим, какие изменения в языке нужны, если бы ссылки были изменяемые, то есть допускали нулевое значение и перенаправление на другую переменную. Первая проблема – это как быть с присваиванием. Нужно отличать присваивание самих ссылок и присваивание объектов, на которые она ссылается. Для изменяемых ссылок логично реализовывать присваивание как присваивание самих ссылок (а как иначе организовать перенаправление?), но присваивание объектов тоже очень важно в C++. Вторая проблема – это использование самих ссылок в качестве выходного параметра (это еще один вариант реализовать перенаправление). Для этого надо было бы разрешить указатели на ссылку и ссылку на ссылку. Решение всех этих проблем усложнило бы и без того непростой C++, то есть неизменяемость C++ ссылок позволило не переусложнить язык. Теперь посмотрим, что происходит в языках со сборкой мусора, например C#. Объекты, управляемые сборщиком мусора, доступны только через ссылку, таким образом, никаких коллизий между объектом и ссылкой на него быть не может, не нужен оператор разыменования. Ссылки на объекты, управляемые сборщиком мусора, являются изменяемыми, то есть могут быть нулевыми и при присваивании происходит присваивание самих ссылок. Но при этом сами объекты не поддерживают присваивание, для них нельзя перегрузить оператор =, поэтому коллизий не возникает. Если для объекта требуется операция, аналогичная копированию или присваиванию, то в C# надо реализовать специальные методы, типа IClonable.Clone(). Но потребность в таких операциях возникает крайне редко. Ссылки на объекты, управляемые сборщиком мусора, сами могут быть выходными параметрами функции, для таких параметров при объявлении и вызове надо использовать ключевое слово ref.
Ваши аргументы за левое расположение cv-qual понятны, они базируются на правилах объявления нескольких переменных в одной инструкции. Но как раз эта возможность — объявлять несколько переменных в одной инструкции, — многими авторами относятся к нерекомендуемым. А если объявлять одну переменную в инструкции, то появляются дополнительные аргументы для правого расположения cv-qual. Именно такой точки зрения придерживаются авторы [VJG].
Тот вариант объявления, который вы привели, фактически описан в раздела 4.2. Объявление нескольких переменных в одной инструкции. Правда я не стал приводить описание в общем виде, а описал ситуацию на «на пальцах», но с подробным примером. Все предыдущие описания относились к объявлению одной переменной.
Согласен, что термин стандартный возможно не очень удачный. Но этот паттерн должен знать каждый программист
Если
static
применяется к локальной переменной, то тут вы правы. Ноstatic
может применяться к членам класса и к глобальным переменным, в этом случае смысл этого ключевого слова другой. Это кратко описано в разделе 5.2.Да, виноват, немного поспешил,
extern
не игнорируется. Даже есть гипотеза, как это работает, но это надо проверять. Сам бы я никогда не написал бы подобный код и тратить время на его исследование особого желания нет.thread_local
иextern
можно сочетать в одном объявлении (компилятор и компоновщик не ругаются). Судя по всему (по тултипу и отсутствие ошибок компоновщика)extern
в данном случае просто игнорируется. Увы, в C++ довольно много подобных малопонятных с первого взгляда конструкций и их следует избегать. Как писал Герб Саттер не суйтесь в темные закоулку C++, пишите код в котором вы полностью уверены.Некоторые встроенные типы в С++ используют несколько ключевых слов. Порядок этих слов стандартом не фиксирован и поэтому следующие три объявления объявляют переменные одного и того же типа.
unsigned long long d1;
long unsigned long d2;
long long unsigned d3;
Классический синтаксис — это первый вариант, он появляется в сообщениях компилятора и его следует придерживаться, чтобы не запутывать себя и потенциальных читателей кода.
Да, стандартный правильный паттерн реализации присваивания использует идиому копирование-обмен. Сначала создается временный объект с новым состоянием, потом происходит обмен состояний этого временного объекта с исходным объектом и после этого неявно вызывается деструктор для временного объекта, который уже будет содержать состояние исходного объекта. Об этом подробно написано в упомянутой статье про перегрузку операторов.
Для пользовательских классов программист должен сам реализовать присваивание (правда иногда можно доверить генерацию присваивания компилятору). Правильно реализованное присваивание использует деструктор для старого состояния и конструктор для нового, то есть это комплексная операция. Если не использовать деструктор, то мы получить утечку ресурсов. Так что говорить, что вызов деструктора, не относится к операции присвоения мне кажется не вполне корректно.
Вот как можно описать семантику присваивания
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/.
int typedef a
;Это то же самое, что
typedef int b;
Последний вариант это классический синтаксис. Первый вариант это некие допустимые вариации, которых в C++ довольно много и их лучше не использовать, чтобы не запутать себя и потенциальных читателей кода.
Да.
И еще
std::string s{ "meow" };
std::string s = { "meow" };
auto s = std::string("meow");
auto s = std::string{ "meow" };
Все эти варианты вызывают один и тот же конструктор.
Присваивание применяется к ранее созданным объектам и поэтому включает обязательный этап — освобождение ресурсов этого объекта. Конечно, для простых переменных типа int ничего освобождать не надо, но для более сложных, то том числе и для std::string, это делать обязательно. А вот при инициализации этого делать не нужно, так как мы инициализируем пустой объект, то есть инициализация более простая процедура. При инициализации работает конструктор объекта а при присваивании оператор присваивания, это разные сущности (хотя и связанные).
Полностью согласен.
Здравствуйте,
Вызов ::operator new(size) — это вызов стандартной функции выделения памяти, которая в качестве параметра принимает требуемый размер. Эта фунция только выделяет память, ничего больше.
Вызов ::new(size) не будет компилироваться, так как в этом случае синтаксически правильной формой должна быть ::new X(/* аргументы конструктора */), где X — это имя класса. В данном случае ::new — это оператор new, который не только выделяет память, но и вызывает конструктор, то есть инициализирует объект.
С уважением. Дмитрий Пономарев.
Спасибо, рад, что мои материалы оказались полезными.
Спасибо за комментарий, очень образное описание сути интерфейса. Многие языки программирования поддерживают интерфейсы на уровне языка (например C#, Java), но вот C++ нет. Приходится эмулировать интерфейсы с помощью чисто виртуальных функций, но при этом возникают достаточно много тонкостей и потенциальных проблем. В своей статье я как раз и попытался разобраться с этими проблемами.
Вообще надо различать вывод аргумента шаблона и вывод типа параметра функции. Они могут не совпадать, типа параметра функции может быть украшен спецификатором ссылки,
const
и другими модификаторами. В шаблонах функций аргумента шаблона нам доступен, но в лямбдах сauto
он будет скрытым.Спасибо за конструктивную критику.
А вот использовать ссылку в качестве параметра функции безопасно
Здесь я имел в виду передача ссылки в глубину стека, конечно возврат этой же ссылки может все сломать и я про это писал.
И про "ссылку" получаемую из итератора
std::vector<bool> ...
Согласен, что итераторы, возвращающие прокси-объекты, не обсуждал. Эта тема подробно обсуждается у Скотта Мейерса.
Не сказано так же о том, что взятие ссылки на временный объект продлевает его срок жизни до конца жизни самой ссылки.
Не согласен, этому посвящен раздел 4.1.
Одна из идей, которой руководствуются при проектировании ссылок – это сделать ссылку максимально неотличимой от объекта, на который она ссылается. Этим можно объяснить, почему при инициализации C++ ссылки не используется специальный оператор (типа
&
в случае указателей) и не используется оператор разыменования (типа*
в случае указателей) для доступа к объекту. В результате возникают некоторые коллизии, но небольшие, например, нельзя перегрузить функции, у которых параметр передается по значению и по ссылке на константу.Теперь про неизменяемость C++ ссылок, то есть отсутствие нулевых ссылок (обязательная инициализация) и невозможность перенаправить на другую переменную. Посмотрим, какие изменения в языке нужны, если бы ссылки были изменяемые, то есть допускали нулевое значение и перенаправление на другую переменную. Первая проблема – это как быть с присваиванием. Нужно отличать присваивание самих ссылок и присваивание объектов, на которые она ссылается. Для изменяемых ссылок логично реализовывать присваивание как присваивание самих ссылок (а как иначе организовать перенаправление?), но присваивание объектов тоже очень важно в C++. Вторая проблема – это использование самих ссылок в качестве выходного параметра (это еще один вариант реализовать перенаправление). Для этого надо было бы разрешить указатели на ссылку и ссылку на ссылку. Решение всех этих проблем усложнило бы и без того непростой C++, то есть неизменяемость C++ ссылок позволило не переусложнить язык.
Теперь посмотрим, что происходит в языках со сборкой мусора, например C#. Объекты, управляемые сборщиком мусора, доступны только через ссылку, таким образом, никаких коллизий между объектом и ссылкой на него быть не может, не нужен оператор разыменования. Ссылки на объекты, управляемые сборщиком мусора, являются изменяемыми, то есть могут быть нулевыми и при присваивании происходит присваивание самих ссылок. Но при этом сами объекты не поддерживают присваивание, для них нельзя перегрузить оператор
=
, поэтому коллизий не возникает. Если для объекта требуется операция, аналогичная копированию или присваиванию, то в C# надо реализовать специальные методы, типаIClonable.Clone()
. Но потребность в таких операциях возникает крайне редко. Ссылки на объекты, управляемые сборщиком мусора, сами могут быть выходными параметрами функции, для таких параметров при объявлении и вызове надо использовать ключевое словоref
.