что такое nullptr c

kalnitsky’s way

Как и обещал, я продолжаю публикации на тему нового стандарта C++11. В прошлой статье я рассказал о таких вещах как:

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

nullptr

В C++11 для обнуления указателей появилось специальное ключевое слово nullptr. В более ранних стандартах, официально использовалась запись:

Либо же вариант с макросом NULL, перекочевавшим из языка C. Проблема очевидна: для обнуления указателя используется целое число, из-за чего могут возникать мелкие неприятности. Например, при перегрузке функции:

Какой вариант функции вызовется? Очевидно, что первый, принимающий целое число. А мы, может, хотели вызвать второй вариант, только с нулевым указателем. Для решения этой проблемы пришлось бы кастовать 0 к указателю:

На первый взгляд — все просто отлично! Но ошибка есть, и она такая же как и в предыдущем варианте. std::fill является шаблоном, и увидев 0, шаблон примет его за int и, конечно же, из-за несоответствия типов мы получим очень страшное сообщение об ошибке от компилятора. Выход был такой же — кастовать 0 к указателю, что уж явно не повышает читабельность.

Именно поэтому, было принято новое ключевое слово, и имя ему nullptr. Используя это ключевое слово, мы избавимся от вышеописанной проблемы, так как nullptr имеет свой собственный тип — std::nullptr_t — и компилятор не спутает его ни с чем другим.

Список инициализации

В предыдущем стандарте возможности списков инициализации были чрезвычайно малы. Что мы могли сделать раньше? Лишь проинициализировать некоторую структуру, да некоторый массив:

Но C++ предоставляет более удобные, более гибкие средства разработки. Я говорю о классах и контейнерах.

C++11 наконец разрешает эту несправедливость, путем введения шаблонного класса std::initializer_list<>. Все контейнеры отныне обладают конструктором, принимающим список инициализации ( std::initializer_list<> ), отчего становится реальной следующая запись:

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

Объекты std::initializer_list<> не могут быть изменены.

Универсальная инициализация

Списки инициализации — это хорошо. Но разработчики решили на этом не останавливаться и пошли еще дальше. Они расширили синтаксис списка инициализации, позволив вытворять следующие вещи:

Подобная инициализация вызовет конструктор, как будто бы мы написали:

Универсальная инициализация работает как для классов, так и для структур. В случае классов вызовется конструктор, а в случае структур — будет происходить поэлементная инициализация в порядке объявления атрибутов.

Если не указать последний атрибут (или атрибуты), то для него вызовется конструктор по-умолчанию. Для встроенного типа (например int) произойдет инициализация нулем.

Стоит отметить, что такая инициализация позволяет писать следующие вещи:

Интересен тот факт, что универсальная инициализация защищает от неявных преобразований.

Познакомившись с универсальной инициализацией и списками инициализации, может возникнуть вопрос: «А какой конструктор вызовется в следующей ситуации?»

В этом случае, при создании объекта foo вызовется конструктор Foo(int x, int y), а при создании bar — Foo(std::initializer_list list). В случае, если последний конструктор будет отсутствовать, то все пойдет как обычно: в обеих случаях вызовется первый конструктор.

Вместо заключения

Хотелось написать больше, но получилось совсем чуть-чуть. Найти время для творчества — это действительно проблема. Ну что же, самые широко известные (за исключением лямбд) нововведения я затронул. В дальнейшем напишу о менее известный вещах.

Источник

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

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

#1 — auto

До С++11, ключевое слово auto использовалось как спецификатор хранения переменной (как, например, register, static, extern ). В С++11 auto позволяет не указывать тип переменной явно, говоря компилятору, чтобы он сам определил фактический тип переменной, на основе типа инициализируемого значения. Это может использоваться при объявлении переменных в различных областях видимости, как, например, пространство имен, блоки, инициализация в цикле и т.п.

#2 — nullptr

#3 — range-based циклы

Это полезно, когда вы просто хотите получить элементы массива/контейнера или сделать с ними что-то, не заботясь об индексах, итераторах или кол-ве элементов.

#4 — override и final

Мне всегда не нравились виртуальные функции в С++. Ключевое слово virtual опционально и поэтому немного затрудняло чтение кода, заставляя вечно возвращаться в вершину иерархии наследования, чтобы посмотреть объявлен ли виртуальным тот или иной метод. Я всегда использовал этой ключевое слово так же и в производных классах (и поощрял людей, кто так делал), чтобы код был понятнее. Тем не менее, есть ошибки, которые могут все таки возникнуть. Возьмем следующий пример:

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

Теперь это вызовет ошибку при компиляции (точно так же, если бы вы использовали override во втором примере):

#5 — строго-типизированный enum

У «традиционных» перечислений в С++ есть некоторые недостатки: они экспортируют свои значения в окружающую область видимости (что может привести к конфликту имен), они неявно преобразовываются в целый тип и не могут иметь определенный пользователем тип.

#6 — интеллектуальные указатели

Первое объявление эквивалентно следующему:

make_shared — это функция, имеющая преимущество при выделении памяти для совместно используемого объекта и интеллектуального указателя с единственным выделением, в отличие от явного получения shared_ptr через конструктор, где требуется, по крайней мере, два выделения. Из-за этого может произойти утечка памяти. В следующем примере как раз это демонстрируется, утечка может произойти в случае, если seed() бросит исключение.

#7 — лямбды

#8 — non-member begin() и end()

Давайте возьмем, например, предыдущий пример, где я выводил вектор и затем искал первый нечетный элемент. Если std::vector заменить С-подобным массивом, то код будет выглядеть так:

С begin() и end() его можно переписать следующим образом:

#9 — static_assert и классы свойств

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

Однако, при компиляции не возникнет ошибки, если написать следующее:

#10 — семантика перемещения

Это — еще одна важная тема, затронутая в С++11. На эту тему можно написать несколько статей, а не абзацев, поэтому я не буду сильно углубляться.

C++11 ввел понятие rvalue ссылок (указанных с &&), чтобы отличать ссылка на lvalue (объект, у которого есть имя) и rvalue (объект, у которого нет имени). Семантика перемещения позволяет изменять rvalues (ранее они считались неизменными и не отличались от типов const T&).

Класс/структура раньше имели некоторые неявные функции-члены: конструктор по умолчанию (если другой конструктор не определен), конструктор копирования и деструктор. Конструктор копирования выполняет поразрядное копирование переменных. Это означает, что если у вас есть класс с указателями на какие-то объекты, то конструктор копирования скопирует указатели, а не объекты, на которые они указывают. Если вы хотите получить в копии именно объекты, а не лишь указатели на них, вы должны это явно описать в конструкторе копирования.

Конструктор перемещения и оператор присваивания перемещения — эти две специальные функции принимают параметр T&&, который является rvalue. Фактически, они могут изменять объект.

Следующий пример показывает фиктивную реализацию буфера. Буфер идентифицируется именем, имеет указатель (обернутый в std::unique_ptr ) на массив элементов типа Т и переменную, содержащую размер массива.

Конструктор копирования по умолчанию и оператор присваивания копии должны быть вам знакомы. Новое в С++11 — это конструктор перемещения и оператор присваивания перемещения, Если вы выполните этот код, то увидите, что когда создается b4 — вызывается конструктор перемещения. Кроме того, когда b1 присваивается значение — вызывается оператор присваивания перемещения. Причина — значение, возвращаемое функцией getBuffer() — rvalue.

Источник

Каковы преимущества использования nullptr?

этот кусок кода принципиально делает то же самое для трех указателей (безопасная инициализация указателя):

7 ответов

в этом коде, кажется, нет преимущества. Но рассмотрим следующие перегруженные функции:

таким образом, решение таких проблем заключается в использовании nullptr :

что это NULL точно?

Pre C++11 NULL использовался для представления указателя, который не имеет значения или указателя, который не указывает на что-либо действительное. Вопреки распространенному мнению, NULL не является ключевым словом в C++. Это идентификатор, определенный в заголовках стандартной библиотеки. Короче говоря, вы не можете использовать NULL без включения некоторых стандартных заголовков библиотеки. Рассмотрим образец программа:

выход:

C++ не смог принять эту спецификацию как она есть. В отличие от C, C++ является строго типизированным языком (C не требует явного приведения из void* для любого типа, в то время как C++ требует явного приведения). Это делает определение NULL, указанное стандартом C, бесполезным во многих выражениях c++. Например:

так зачем нужна еще одна константа нулевого указателя, когда у нас есть NULL уже?

хотя комитет по стандартам C++ придумал нулевое определение, которое будет работать для C++, это определение имело свою долю проблем. НОЛЬ сработало достаточно хорошо почти для всех сценариев, но не для всех. Это дало неожиданные и ошибочные результаты для некоторых редких сценариев. :

выход:

очевидно, что намерение состоит в том, чтобы вызвать версию, которая принимает char* в качестве аргумента, но в качестве вывода показывает функцию, которая принимает int версия вызывается. Это потому, что NULL является числовым буквальный.

кроме того, поскольку он определяется реализацией, является ли NULL 0 или 0L, может быть много путаницы в разрешении перегрузки функции.

Пример Программы:

анализируя приведенный выше фрагмент:

так, в зависимости от реализации, Один и тот же код может давать разные результаты, что явно нежелательно. Естественно, комитет по стандартам C++ хотел исправить это и это основная мотивация для nullptr.

рассмотрим следующий пример:

Читайте также:  что значит высокие скулы

в вышеуказанной программе,

таким образом, введение nullptr позволяет избежать всех проблем старого доброго NULL.

основное правило C++11 просто начать использовать nullptr всякий раз, когда вы в противном случае использовали бы NULL в прошлом.

Стандартные Ссылки:

Стандарт C++11: C. 3.2.4 макрос NULL
Стандарт C++11: 18.2 Типы
стандарт C++11: 4.10 преобразования указателей
Стандарт C99: 6.3.2.3 Указатели

реальная мотивация здесь идеальный переадресации.

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

вызывает две отдельные перегрузки. Кроме того, рассмотрим

это неоднозначно. Но, с nullptr, вы можете предоставить

основы nullptr

литерал 0 является int, а не указателем. Если C++ обнаруживает, что смотрит на 0 в контексте, где можно использовать только указатель, он неохотно интерпретирует 0 как нулевой указатель, но это резервная позиция. Основная политика C++ это 0-int, а не указатель.

Преимущество 1-Удалите двусмысленность при перегрузке указателя и интегральных типов

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

преимущество nullptr заключается в том, что он не имеет интегрального типа. Вызов перегруженной функции fun с nullptr вызывает перегрузку void* (т. е. перегрузку указателя), потому что nullptr нельзя рассматривать как что-либо интегральное:

использование nullptr вместо 0 или NULL позволяет избежать сюрпризов разрешения перегрузки.

еще одним преимуществом nullptr над NULL(0) при использовании auto для типа возврата

например, предположим, что вы столкнулись с этим в базовый код:

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

нет сомнений: результат должен быть типом указателя.

преимущество 3

код пересчитывается, как показано ниже:

подробный анализ, почему компиляция не удалась для lockAndCall(f1, f1m, 0) & lockAndCall(f3, f3m, nullptr) не lockAndCall(f3, f3m, nullptr)

почему компиляция lockAndCall(f1, f1m, 0) & lockAndCall(f3, f3m, nullptr) не удалось?

нет никакого прямого преимущества наличия nullptr так, как вы показали примеры.
Но рассмотрим ситуацию, когда у вас есть 2 функции с одинаковыми именами; занимает 1 int и еще один int*

если вы хотите позвонить foo(int*) передавая NULL, то путь:

nullptr делает его более простое и интуитивное:

дополнительная ссылка от Бьярне страница.
Не имеет значения, но на C++11 Примечание:

просто как уже говорили другие, его основное преимущество заключается в перегрузках. И при этом явный int против указателя перегрузок могут быть редкими, рассмотрим стандартные библиотечные функции как std::fill (который не раз кусал меня в C++03):

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

Источник

nullptr (C++/CLI and C++/CX)

The nullptr keyword represents a null pointer value. Use a null pointer value to indicate that an object handle, interior pointer, or native pointer type does not point to an object.

Use nullptr with either managed or native code. The compiler emits appropriate but different instructions for managed and native null pointer values. For information about using the ISO standard C++ version of this keyword, see nullptr.

The nullptr keyword is equivalent to Nothing in Visual Basic and null in C#.

Usage

The nullptr keyword can be used anywhere a handle, native pointer, or function argument can be used.

The nullptr keyword is not a type and is not supported for use with:

throw nullptr (although throw (Object^)nullptr; will work)

The nullptr keyword can be used in the initialization of the following pointer types:

Windows Runtime handle

Managed interior pointer

The nullptr keyword can be used to test if a pointer or handle reference is null before the reference is used.

Function calls among languages that use null pointer values for error checking should be interpreted correctly.

Example: nullptr keyword

The following code example demonstrates that the nullptr keyword can be used wherever a handle, native pointer, or function argument can be used. And the example demonstrates that the nullptr keyword can be used to check a reference before it is used.

Example: Use nullptr and zero interchangeably

The following code example shows that nullptr and zero can be used interchangeably on native pointers.

Example: Interpret nullptr as a handle

The following code example shows that nullptr is interpreted as a handle to any type or a native pointer to any type. In case of function overloading with handles to different types, an ambiguity error will be generated. The nullptr would have to be explicitly cast to a type.

Example: Cast nullptr

The following code example shows that casting nullptr is allowed and returns a pointer or handle to the cast type that contains the nullptr value.

Example: Pass nullptr as a function parameter

The following code example shows that nullptr can be used as a function parameter.

Example: Default initialization

Example: Assign nullptr to a native pointer

Requirements

Compiler option: (Not required; supported by all code generation options, including /ZW and /clr )

Источник

Как избежать ошибок, используя современный C++

Одной из проблем C++ является большое количество конструкций, поведение которых не определено или просто неожиданно для программиста. С такими ошибками мы часто сталкиваемся при использовании статического анализатора кода на разных проектах. Но, как известно, лучше всего находить ошибки ещё на этапе компиляции. Посмотрим, какие техники из современного C++ позволяют писать не только более простой и выразительный код, но и сделают наш код более безопасным и надёжным.

Что такое Modern C++?

Термин Modern C++ стал очень популярен после выхода С++11. Что он означает? В первую очередь, Modern C++ — это набор паттернов и идиом, которые призваны устранить недостатки старого доброго «C с классами», к которому привыкли многие C++ программисты, особенно если они начинали программировать на C. Код на C++11 во многих случаях выглядит более лаконично и понятно, что очень важно.

Что обычно вспоминают, когда говорят о Modern C++? Параллельность, compile-time вычисления, RAII, лямбды, диапазоны (ranges), концепты, модули и другие не менее важные компоненты стандартной библиотеки (например, API для работы с файловой системой). Это очень крутые нововведения, и мы их ждём в следующих стандартах. Вместе с тем, хочется обратить внимание, как новые стандарты позволяют писать более безопасный код. При разработке статического анализатора кода мы встречаемся с большим количеством разных типов ошибок и порой возникает мысль: «А вот в современном C++ можно было бы этого избежать». Поэтому предлагаю рассмотреть серию ошибок, найденных нами с помощью PVS-Studio в различных Open Source проектах. Заодно и посмотрим, как их лучше поправить.

Автоматическое выведение типа

В C++11 были добавлены ключевые слова auto и decltype. Вы конечно же знаете, как они работают:

С помощью auto можно очень удобно сокращать длинные типы, при этом не теряя в читаемости кода. Однако по-настоящему эти ключевые слова раскрываются в сочетании с шаблонами: c auto или decltype не нужно явно указывать тип возвращаемого значения.

Но вернёмся к нашей теме. Вот пример 64-битной ошибки:

В 64-битном приложении значение string::npos больше, чем максимальное значение UINT_MAX, которое вмещает переменная типа unsigned. Казалось бы это тот самый случай, где auto может нас спасти от подобного рода проблем: нам не важен тип переменной n, главное, чтобы он вмещал все возможные значения string::find. И действительно, если мы перепишем этот пример с auto, то ошибка пропадёт:

Но здесь не всё так просто. Использование auto не панацея и существует множество ошибок, связанных с ним. Например, можно написать такой код:

auto не спасёт от переполнения и памяти под буфер будет выделено меньше 5GiB.

В распространённой ошибке с неправильно записанным циклом, auto нам также не помощник. Рассмотрим пример:

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

Можно ли этот фрагмент переписать через auto?

Нет, ошибка никуда не делась. Стало даже хуже.

С простыми типами auto ведёт себя из рук вон плохо. Да, в наиболее простых случаях (auto x = y) оно работает, но как только появляются дополнительные конструкции, поведение может стать более непредсказуемым. И что самое худшее, ошибку будет труднее заметить, так как типы переменных будут неочевидны на первый взгляд. К счастью для статических анализаторов посчитать тип проблемой не является: они не устают и не теряют внимания. Но простым смертным лучше всё же указывать простые типы явно. К счастью, от сужающего приведения можно избавиться и другими способами, но о них чуть позже.

Опасный countof

Одним из «опасных» типов в C++ является массив. Нередко при передаче его в функцию забывают, что он передаётся как указатель, и пытаются посчитать количество элементов через sizeof:

Примечание. Код взят из Source Engine SDK.

Предупреждение PVS-Studio: V511 The sizeof() operator returns size of the pointer, and not of the array, in ‘sizeof (iNeighbors)’ expression. Vrad_dll disp_vrad.cpp 60

Такая путаница может возникнуть из-за указания размера массива в аргументе: это число ничего не значит для компилятора и является просто подсказкой программисту.

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

В случае, когда мы передаём в эту функцию не массив, мы получаем ошибку компиляции. В C++17 можно использовать std::size.

В C++11 добавили функцию std::extent, но она в качестве countof не подходит, так как возвращает 0 для неподходящих типов.

Читайте также:  что делать если парапроктит вскрылся

Ошибиться можно не только с countof, но и с sizeof.

Примечание. Код взят из Chromium.

Как ошибаются в простом for

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

Посмотрим на фрагменты из проектов:

Примечание. Код взят из Haiku Operation System.

Предупреждение PVS-Studio: V706 Suspicious division: sizeof (kBaudrates) / sizeof (char *). Size of every element in ‘kBaudrates’ array does not equal to divisor. SerialWindow.cpp 162

Такие ошибки мы подробно рассмотрели в предыдущем пункте: опять неправильно посчитали размер массива. Можно легко исправить положение использованием std::size:

Но есть способ получше. А пока посмотрим на ещё один фрагмент.

Примечание. Код взят из Shareaza.

Предупреждение PVS-Studio: V547 Expression ‘nCharPos >= 0’ is always true. Unsigned type value is always >= 0. BugTrap xmlreader.h 946

Типичная ошибка при написании обратного цикла: забыли, что итератор беззнакового типа и проверка возвращает true всегда. Возможно, вы подумали: «Как же так? Так ошибаются только новички и студенты. У нас, профессионалов, таких ошибок не бывает». К сожалению, это не совсем верно. Конечно, все понимают, что (unsigned >= 0)true. Откуда тогда подобные ошибки? Часто они возникают в результате рефакторинга. Представим такую ситуацию: проект переходит с 32-битной платформы на 64-битную. Раньше для индексации использовались int/ unsigned, и было решено заменить их на size_t/ptrdiff_t. И вот в одном месте проглядели и использовали беззнаковый тип вместо знакового.

Что же делать, чтобы избежать такой ситуации в своём коде? Некоторые советуют использовать знаковые типы, как в C# или Qt. Может это и неплохой способ, но если мы хотим работать с большими объёмами данных, то использования size_t не избежать. Есть ли какой-то более безопасный способ обойти массив в C++? Конечно есть. Начнём с самого простого: non-member функций. Для работы с коллекциями, массивами и initializer_list есть унифицированные функции, принцип работы которых вам должен быть хорошо знаком:

Прекрасно, теперь нам не нужно помнить о разнице между прямым и обратным циклом. Не нужно и думать о том, используем мы простой массив или array — цикл будет работать в любом случае. Использование итераторов — хороший способ избавиться от головной боли, но даже он недостаточно хорош. Лучше всего использовать диапазонный for:

Конечно, в диапазонном for есть свои недостатки: он не настолько гибко позволяет управлять ходом цикла и если требуется более сложная работа с индексами, то этот for нам не поможет. Но такие ситуации стоит рассматривать отдельно. У нас ситуация достаточно простая: необходимо пройтись по элементам массива в обратном порядке. Однако уже на этом этапе возникают трудности. В стандартной библиотеке нет никаких вспомогательных классов для range-based for. Посмотрим, как его можно было бы реализовать:

В C++14 можно упростить код, убрав decltype. Можно увидеть, как auto помогает писать шаблонные функции — reversed_wrapper будет работать и с массивом, и с std::vector.

Теперь можно переписать фрагмент следующим образом:

Чем хорош этот код? Во-первых, он очень легко читается. Мы сразу видим, что здесь массив элементов обходится в обратном порядке. Во-вторых, ошибиться намного сложнее. И в-третьих, он работает с любым типом. Это значительно лучше, чем то, что было.

В boost можно использовать boost::adaptors::reverse(arr).

Но вернёмся к исходному примеру. Там массив передаётся парой указатель-размер. Очевидно, что наше решение с reversed для него работать не будет. Что же делать? Использовать классы, наподобие span/array_view. В C++17 есть string_view, предлагаю им и воспользоваться:

string_view не владеет строкой, по сути это обёртка над const char* и длиной. Поэтому в примере кода, строка передаётся по значению, а не по ссылке. Ключевой особенностью string_view является совместимость с разными способами представления строк: const char*, std::string и не нуль-терминированный const char*.

В итоге функция принимает такой вид:

При передаче в функцию важно не забыть про то, что конструктор string_view(const char*) неявный, поэтому можно написать так:

Строка, на которую указывает string_view не обязана быть нуль-терминированной, на что намекает название метода string_view::data, и это нужно иметь в виду при её использовании. При передаче её значения в какую-нибудь функцию из cstdlib, которая ожидает C строку, можно получить undefined behavior. И это можно легко пропустить, если в большинстве случаев, которые вы тестируете, используются std::string или нуль-терминированные строки.

Отвлечёмся от C++ и вспомним старый добрый C. Как там с безопасностью? Ведь в нём нет проблем с неявными вызовами конструкторов и операторов преобразования и нет проблем с разными видами строк. На практике, ошибки часто встречаются в самых простых конструкциях: самые сложные уже тщательно просмотрены и отлажены, так как вызывают подозрения. В то же время простые конструкции часто забывают проверить. Вот пример опасной конструкции, которая пришла к нам ещё из C:

Пример из ядра Linux. Предупреждение PVS-Studio: V556 The values of different enum types are compared: switch(ENUM_TYPE_A) < case ENUM_TYPE_B:… >. libiscsi.c 3501

Обратите внимание на значения в switch-case: одна из именованных констант взята из другого перечисления. В оригинале, естественно, кода и возможных значений значительно больше и ошибка не является столь же наглядной. Причиной тому нестрогая типизация enum — они могут неявно приводиться к int, и это даёт отличный простор для различных ошибок.

В C++11 можно и нужно использовать enum class: с ними такой трюк не пройдёт, и ошибка проявится во время компиляции. В итоге приведённый ниже код не компилируется, что нам и нужно:

Следующий фрагмент не совсем связан с enum, но имеет схожую симптоматику:

Примечание. Код взят из ReactOS.

Да, значения errno объявлены макросами, что само по себе плохая практика в C++ (да и в C тоже), но даже если бы использовали enum, легче бы от этого не стало. Потерянное сравнение никак не проявится в случае enum (и тем более макроса). А вот enum class такого бы не позволил, так как неявного приведения к bool не произойдёт.

Инициализация в конструкторе

Но вернёмся к исконно C++ проблемам. Одна из них проявляется, когда нужно проинициализировать объект схожим образом в нескольких конструкторах. Простая ситуация: есть класс, есть два конструктора, один из них вызывает другой. Выглядит всё логично: общий код вынесен в отдельный метод — никто не любит дублировать код. В чём подвох?

Примечание. Код взят из LibreOffice.

Предупреждение PVS-Studio: V603 The object was created but it is not being used. If you wish to call constructor, ‘this->Guess::Guess(. )’ should be used. guess.cxx 56

А подвох в синтаксисе вызова конструктора. Часто о нём забывают и создают ещё один экземпляр класса, который сразу же будет уничтожен. То есть инициализация исходного экземпляра не происходит. Естественно есть 1000 и 1 способ это исправить. Например, можно явно вызвать конструктор через this или вынести всё в отдельную функцию:

Кстати, явный повторный вызов конструктора, например, через this это опасная игра и надо хорошо понимать, что происходит. Намного лучше и понятней вариант с функцией Init(). Для тех, кто хочет более подробно разобраться с подвохами, предлагаю познакомиться с 19 главой «Как правильно вызвать один конструктор из другого» из этой книги.

Но лучше всего использовать делегацию конструкторов. Так мы можем явно вызвать один конструктор из другого:

У таких конструкторов есть несколько ограничений. Первое: делегируемый конструктор полностью берёт на себя ответственность за инициализацию объекта. То есть, вместе с ним проинициализировать другое поле класса в списке инициализации не выйдет:

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

О виртуальных функциях

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

Метод Derived::Foo нельзя будет вызвать по указателю/ссылке на Base. Но этот пример простой и можно сказать, что так никто не ошибается. А ошибаются обычно так:

Примечание. Код взят из MongoDB.

Предупреждение PVS-Studio: V762 Consider inspecting virtual function arguments. See seventh argument of function ‘query’ in derived class ‘DBDirectClient’ and base class ‘DBClientBase’. dbdirectclient.cpp 61

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

В следующем фрагменте ситуация хитрее. Такой код будет работать, если его скомпилировать как 32-битный, но не будет работать в 64-битном варианте. Изначально в базовом классе параметр был типа DWORD, но потом его исправили на DWORD_PTR. А в унаследованных классах не поменяли. Да здравствует бессонная ночь, отладка и кофе!

Ошибиться в сигнатуре можно и более экстравагантными способами. Можно забыть const у функции или аргумента. Можно забыть, что функция в базовом классе не виртуальная. Можно перепутать знаковый/беззнаковый тип.

В C++11 добавили несколько ключевых слов, которые могут регулировать переопределение виртуальных функций. Нам поможет override. Такой код просто не скомпилируется.

NULL vs nullptr

Использование NULL для обозначения нулевого указателя приводит к ряду неожиданных ситуаций. Дело в том, что NULL — это обычный макрос, который раскрывается в 0, имеющий тип int. Отсюда несложно понять, почему в этом примере выбирается вторая функция:

Но хоть это и понятно, это точно не логично. Поэтому и появляется потребность в nullptr, который имеет свой собственный тип nullptr_t. Поэтому использовать NULL (и тем более 0) в современном C++ категорически нельзя.

Другой пример: NULL можно использовать для сравнения с другими целочисленными типами. Представим, что есть некая WinAPI функция, которая возвращает HRESULT. Этот тип никак не связан с указателем, поэтому и сравнение его с NULL не имеет смысла. И nullptr это подчёркивает ошибкой компиляции, в то время как NULL работает:

Читайте также:  что значит пинг понг

va_arg

Встречаются ситуации, когда в функцию необходимо передать неопределённое количество аргументов. Типичный пример — функция форматированного ввода/вывода. Да, её можно спроектировать так, что переменное количество аргументов не понадобится, но не вижу смысла отказываться от такого синтаксиса, так как он намного удобнее и нагляднее. Что нам предлагают старые стандарты C++? Они предлагают использовать va_list. Какие при этом могут возникнуть проблемы? В такую функцию очень легко передать аргумент не того типа. Или не передать аргумент. Посмотрим подробнее на фрагменты.

Примечание. Код взят из Chromium.

Предупреждение PVS-Studio: V510 The ‘AtlTrace’ function is not expected to receive class-type variable as third actual argument. delegate_execute.cc 96

Тут хотели вывести на печать строку std::wstring, но забыли позвать метод c_str(). То есть тип wstring будет интерпретирован в функции как const wchar_t*. Естественно, ничего хорошего из этого не выйдет.

Примечание. Код взят из Cairo.

Предупреждение PVS-Studio: V576 Incorrect format. Consider checking the third actual argument of the ‘fwprintf’ function. The pointer to string of wchar_t type symbols is expected. cairo-win32-surface.c 130

В этом фрагменте перепутали спецификаторы формата для строк. Дело в том, что в Visual C++ для wprintf %s ожидает wchar_t*, а %S — char*. Примечательно, что эти ошибки находятся в строках, предназначенных для вывода ошибок или отладочной информации — наверняка это редкие ситуации, поэтому их и пропустили.

Примечание. Код взят из CryEngine 3 SDK.

Предупреждение PVS-Studio: V576 Incorrect format. Consider checking the fourth actual argument of the ‘sprintf’ function. The SIGNED integer type argument is expected. igame.h 66

Не менее легко перепутать и целочисленные типы. Особенно, когда их размер зависит от платформы. Здесь, впрочем, всё банальнее: перепутали знаковый и беззнаковый типы. Большие числа будут распечатаны как отрицательные.

Примечание. Код взят из Word for Windows 1.1a.

Предупреждение PVS-Studio: V576 Incorrect format. A different number of actual arguments is expected while calling ‘printf’ function. Expected: 3. Present: 1. dini.c 498

Пример, найденный в рамках одного из археологических исследований. Строка подразумевает наличие трёх аргументов, но их нет. Может так хотели распечатать данные, лежащие на стеке, но делать таких предположений о том, что там лежит, всё же не стоит. Однозначно надо передать аргументы явно.

Примечание. Код взят из ReactOS.

Предупреждение PVS-Studio: V576 Incorrect format. Consider checking the third actual argument of the ‘swprintf’ function. To print the value of pointer the ‘%p’ should be used. dialogs.cpp 66

Пример 64-битной ошибки. Размер указателя зависит от архитектуры и использовать для него %u — плохая идея. Что для использовать вместо него? Сам анализатор подсказывает нам правильный ответ — %p. Хорошо, если указатель просто распечатывают для отладки. Гораздо интереснее будет, если его потом попытаются из буфера прочитать и использовать.

Чем же плохи функции с переменным количеством аргументов? Практически всем! В них нельзя проверить ни тип аргумента, ни количество аргументов. Шаг влево, шаг вправо — undefined behavior.

Хорошо, что есть более надёжные альтернативы. Во-первых, есть variadic templates. С помощью них мы получаем всю информацию о переданных типах во время компиляции и можем это использовать, как захотим. Для примера напишем тот же printf, но чуть более безопасный:

Естественно это всего лишь пример: на практике его использовать бессмысленно. Но с variadic templates вас в реализации ограничивает лишь полёт фантазии, а не средства языка.

Ещё одна конструкция, которую можно рассмотреть, как вариант передачи переменного количества аргументов, — то std::initializer_list. Он не позволяет передать аргументы разных типов. Но если этого достаточно, то можно использовать его:

При этом обходить его очень удобно, так как можно использовать всё те же begin, end и диапазонный for.

Narrowing

Сужающие (narrowing) приведения доставили много головной боли программистам. Особенно, когда стал актуален переход на 64-битную архитектуру. Хорошо, если в коде везде использовались правильные типы. Но не везде всё так радужно: нередко использовались различные грязные хаки и экстравагантные способы хранения указателей. Не один литр кофе был выпит, чтобы найти все такие места.

Но отвлечёмся от 64-битных ошибок. Вот более простой пример: есть два целочисленных значения и хотят найти их отношение. Делают это вот так:

Примечание. Код взят из Source Engine SDK.

Предупреждение PVS-Studio: V636 The expression was implicitly cast from ‘int’ type to ‘float’ type. Consider utilizing an explicit type cast to avoid the loss of a fractional part. An example: double A = (double)(X) / Y;. Client (HL2) detailobjectsystem.cpp 1480

К сожалению, полностью обезопасить себя от таких ошибок не получится — всегда найдётся ещё один способ неявно привести один тип к другому. Но у нового способа инициализации в C++11 есть одна приятная особенность: он запрещает сужающие приведения. В этом коде ошибка возникнет ещё при компиляции и её можно будет легко поправить.

No news is good news

Возможностей ошибиться в управлении памятью и ресурсами великое множество. Удобство при работе с ними — важное требование к современному языку. Современный C++ тут не отстаёт и предлагает целый ряд средств для автоматического контроля ресурсами. И хотя такие ошибки — это скорее вотчина динамического анализа, некоторые проблемы может выявить и статический анализ. Посмотрим на некоторые из них:

Примечание. Код взят из Chromium.

Предупреждение PVS-Studio: V554 Incorrect use of auto_ptr. The memory allocated with ‘new []’ will be cleaned using ‘delete’. interactive_ui_tests accessibility_win_browsertest.cc 171

Естественно, идея умных указателей не нова: например, был такой класс std::auto_ptr. В прошедшем времени я о нём говорю, потому что он объявлен deprecated в C++11, а в C++17 удалён. В этом фрагменте ошибка появилась из-за того, что auto_ptr неправильно использовали: у класса нет специализации для массивов, и будет вызван стандартный delete, а не delete[]. На замену auto_ptr пришёл unique_ptr, у которого есть и специализация для массивов, и возможность передать функтор deleter, который будет вызван вместо delete, и полноценная поддержка перемещающей семантики. Казалось, что здесь может быть не так?

Примечание. Код взят из nana.

Предупреждение PVS-Studio: V554 Incorrect use of unique_ptr. The memory allocated with ‘new []’ will be cleaned using ‘delete’. text_editor.cpp 3137

Оказывается, что можно допустить точно такую же ошибку. Да, достаточно написать unique_ptr и она исчезнет, тем не менее в таком виде код тоже компилируется. То есть таким образом тоже можно ошибиться, а как показывает практика, если где-то можно ошибиться — там обязательно ошибутся. Фрагмент кода это только подтверждает. Так что, используя unique_ptr с массивами, будьте предельно осторожны: выстрелить себе в ногу проще, чем кажется. Может быть тогда лучше использовать std::vector по заветам Modern C++?

Рассмотрим ещё одну разновидность несчастных случаев.

Примечание. Код взят из Unreal Engine 4.

Предупреждение PVS-Studio: V611 The memory was allocated using ‘new T[]’ operator but was released using the ‘delete’ operator. Consider inspecting this code. It’s probably better to use ‘delete [] Code;’. openglshaders.cpp 1790

Ту же ошибку легко допустить и без умных указателей: память, выделенную при помощи new[], освобождают через free.

Примечание. Код взят из CxImage.

Предупреждение PVS-Studio: V611 The memory was allocated using ‘new’ operator but was released using the ‘free’ function. Consider inspecting operation logics behind the ‘ptmp’ variable. ximalyr.cpp 50

А в этом фрагменте перепутали malloc/free и new/delete. Такое может случиться при рефакторинге: были везде функции из C, решили поменять, получили UB.

Примечание. Код взят из Fennec Media.

Предупреждение PVS-Studio: V575 The null pointer is passed into ‘free’ function. Inspect the first argument. settings interface.c 3096

А это уже более занятный пример. Существует практика, в который указатель обнуляют после освобождения. Иногда даже специальные макросы для этого пишут. Замечательная практика с одной стороны: так можно обезопасить себя от повторного освобождения памяти. Но тут напутали порядок выражений и в free приходит уже нулевой указатель (что и замечает статический анализатор).

Но проблема относится не только к управлению памятью, но и к управлению ресурсами. Можно, например, забыть закрыть файл, как во фрагменте выше. И ключевое слово в обоих случаях — RAII. Эта же концепция стоит и за умными указателями. В сочетании с move-semantics RAII позволяет избавиться от многих ошибок, связанных с утечками памяти. Да и код, написанный в таком стиле, позволяет более наглядно определить владение ресурсом.

В качестве небольшого примера приведу обёртку над FILE, использующую возможности unique_ptr:

Но для работы с файлами скорее всего захочется иметь более функциональную обёртку (да и с более понятным синтаксисом). Самое время вспомнить, что в C++17 добавят API для работы с файловыми системами — std::filesystem. Но если это решение вас не устраивает и вам хочется использовать fread/fwrite вместо i/o-потоков, то можно вдохновиться unique_ptr и написать свой File, оптимизированный под свои нужды и вместе с тем удобный, читаемый и безопасный.

Что же в итоге?

Современный C++ привнёс много средств, которую помогут писать код более безопасно. Появилось много конструкций для compile-time вычислений и проверок. Можно перейти на более удобную модель управления памятью и ресурсами.

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

К слову об инструментах. Предлагаю попробовать PVS-Studio: недавно мы начали разрабатывать версию под Linux и вы её можете попробовать в деле: она поддерживает любую сборочную систему и позволяет легко проверить проект, просто собрав его. А для Windows-разработчиков у нас есть удобный плагин для Visual Studio, который вы можете попробовать в trial-версии.

Если хотите поделиться этой статьей с англоязычной аудиторией, то прошу использовать ссылку на перевод: Pavel Belikov. How to avoid bugs using modern C++.

Источник

Строительный портал