Читать «Эффективный и современный С++. 42 рекомендации по использованию С++11 и С++14» онлайн - страница 46

Скотт Мейерс

void f(int); // Три перегрузки функции f

void f(bool);

void f(void*);

f(0);        // Вызов f(int), не f(void*)

f(NULL);     // Может не компилироваться, но обычно

             // вызывает f(int) и никогда - f(void*)

Неопределенность в отношении поведения f(NULL) является отражением свободы предоставленной реализациям в отношении типа NULL. Если NULL определен, например, как 0L (т.e. 0 как значение типа long), то вызов является неоднозначным, поскольку преобразования long в int, long в bool и 0L в void* рассматриваются как одинаково подходящие. Интересно, что этот вызов является противоречием между видимым смыслом исходного текста (“вызываем f с нулевым указателем NULL”) и фактическим смыслом (“вызываем f с некоторой разновидностью целых чисел — не указателем”). Это противоречащее интуиции поведение приводит к рекомендации программистам на С++98 избегать перегрузки типов указателей и целочисленных типов. Эта рекомендация остается в силе и в С++11, поскольку, несмотря на рекомендации данного раздела, некоторые разработчики, определенно, продолжат применять 0 и NULL, несмотря на то что nullptr является лучшим выбором.

Преимущество nullptr заключается в том, что это значение не является значением целочисленного типа. Честно говоря, он не имеет и типа указателя, но его можно рассматривать как указатель любого типа. Фактическим типом nullptr является std::nullptr_t, ну, а тип std::nullptr_t циклически определяется как тип значения nullptr… Тип std::nullptr_t неявно преобразуется во все типы обычных указателей, и именно это делает nullptr действующим как указатель всех типов.

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

f(nullptr); // Вызов f(void*)

Использование nullptr вместо 0 или NULL, таким образом, позволяет избежать сюрпризов перегрузки, но это не единственное его преимущество. Оно позволяет также повысить ясность кода, в особенности при применении auto-переменных. Предположим, например, что у нас есть следующий исходный текст:

auto result = findRecord( /* Аргументы */ );

if (result == 0) {

 …

}

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

auto result = findRecord( /* Аргументы */ );

if (result == nullptr) {

 …

}

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

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

int f1(std::shared_ptr<Widget> spw);    // Вызывается только при

double f2(std::unique_ptr<Widget> upw); // блокировке соответ-

bool f3(Widget* pw);                    // ствующего мьютекса