В чем разница между передачей NULL и nullptr в параметр шаблона?

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

template <typename Func, typename... Args>
auto awrap(Func &func, Args&&... args)
{
    auto code = func(args...);

    if (code >= 0)
        return code;

    ... handle the error ...
};
...
awrap(handlepath, handle, path, NULL, 0, coll, NULL);

Вышеуказанное отлично компилируется с clang, но g++13 и Microsoft VC++ жалуются на два NULL-аргумента:

... error: invalid conversion from 'int' to 'const char*' [-fpermissive]
   87 |                 auto code = func(args...

Замена двух NULL на nullptr решает проблему, но какое это имеет значение?

Скорее всего, препроцессор где-то преобразует NULL в 0x0 или даже 0, но исходный вызов никогда не поднимал «бровь». Использование NULL идеально подходило в:

handlepath(handle, path, NULL, 0, coll, NULL);

Почему это проблема (для некоторых компиляторов) при использовании в оболочке?


ОБНОВЛЕНИЕ: /usr/include/sys/_null.h в моей системе FreeBSD имеет следующий код:

#ifndef NULL

#if !defined(__cplusplus)
#define NULL    ((void *)0)
#else
#if __cplusplus >= 201103L
#define NULL    nullptr
#elif defined(__GNUG__) && defined(__GNUC__) && __GNUC__ >= 4
#define NULL    __null
#else
#if defined(__LP64__)
#define NULL    (0L)
#else
#define NULL    0
#endif  /* __LP64__ */
#endif  /* __GNUG__ */
#endif  /* !__cplusplus */

#endif

Так:

  1. для C NULL — это (void *)0;

  2. для clang++ NULL и nullptr — одно и то же, тогда как для GNU это может быть не так...

NULL определяется реализацией и может быть определен как 0 (для устаревших ситуаций) или nullptr (лучше). Но, возможно, вы все равно намеревались nullptr? Другими словами, какова ваша мотивация вообще использовать NULL?
Wyck 04.09.2024 20:56

Оборачиваемая функция — handlepath() — относится к C, и все предоставленные поставщиком примеры используют NULL.

Mikhail T. 04.09.2024 21:17
constexpr auto ø = nullptr; теперь вы можете использовать ø.
Eljay 04.09.2024 21:26

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

Red.Wave 05.09.2024 12:41

И в C, и в C++ (ссылаясь на C) NULL требуется только для расширения до константы нулевого указателя, которая представляет собой значение, которое сравнивается с нулем - тип не указан. На практике некоторые реализации определяют его как 0 или 0L или аналогичный, поэтому он имеет целочисленный тип, но другие определяют его как указательный тип (например, он расширяется до (void *)0). В C++ nullptr гарантированно имеет тип std::nullptr_t, который можно неявно преобразовать в указатель. Это важно для пакетов параметров шаблона, поскольку они не выполняют никаких неявных преобразований (например, для преобразования 0 в указатель).

Peter 05.09.2024 17:03
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
9
5
471
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Ответ принят как подходящий

Во многих реализациях NULL — это просто #define для целочисленного литерала 0, например:

#define NULL 0

Вы можете напрямую присвоить литерал 0 любому указателю, поэтому передача NULL непосредственно в целевую функцию работает нормально.

Поскольку вы передаете NULL в параметр шаблона, этот параметр выводится как тип int, как сказано в сообщении об ошибке.

В этом вызове:

awrap(handlepath, handle, path, NULL, 0, coll, NULL);

awrap() разрешится примерно так:

auto awrap(decltype(handlepath) &func, decltype(handle) arg1, decltype(path) arg2,
    int arg3, int arg4, decltype(coll) arg5, int arg6)
 // ^^^^^^^^                                 ^^^^^^^^
{
    auto code = func(arg1, arg2, arg3, arg4, arg5, arg6);
    //                    error: ^^^^       error: ^^^^
    ...
};

Чтобы присвоить переменную int указателю, вам необходимо явное приведение типа, которое вы не используете.

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

В этом вызове:

awrap(handlepath, handle, path, nullptr, 0, coll, nullptr);

awrap() разрешится примерно так:

auto awrap(decltype(handlepath) &func, decltype(handle) arg1, decltype(path) arg2,
    nullptr_t arg3, int arg4, decltype(coll) arg5, nullptr_t arg6)
 // ^^^^^^^^^^^^^^                                 ^^^^^^^^^^^^^^
{
    auto code = func(arg1, arg2, arg3, arg4, arg5, arg6);
    //                       OK: ^^^^          OK: ^^^^
    ...
};

Итак, при вызове вашего шаблона следует использовать nullptr.

Но если вы хотите использовать NULL, вы должны привести его к (char*)NULL (или эквивалентному) или к любому другому типу указателя, который ожидает параметр, например:

awrap(handlepath, handle, path, (char*)NULL, 0, coll, (char*)NULL);

До сих пор не понимаю, почему передача NULL была в порядке до переноса. И почему clang здесь даже не предупреждает (а тем более не ошибается)...

Mikhail T. 04.09.2024 20:58

Проголосовал за, увидев «Во многих реализациях». Спасибо за соответствующую квалификацию.

Wyck 04.09.2024 21:00

@МихаилТ. как я объяснил, передача литерала 0 в указатель работает. Таким образом, передача NULL непосредственно в параметр-указатель исходной функции будет работать. Но ваша оболочка добавляет уровень косвенности, вы больше не передаете литерал 0 непосредственно в параметр-указатель исходной функции. Теперь вы передаете NULL в (выведенный) int параметр оболочки, а затем передаете его int в параметр-указатель исходной функции. Что не работает без приведения типов.

Remy Lebeau 04.09.2024 21:08

«(char*)NULL (он же reinterpret_cast<char*>(0))»: Это неправильно. (char*)NULL разрешится в static_cast<char*>(NULL), который использует неявное преобразование. На самом деле технически не гарантировано, что reinterpret_cast<char*>(0) будет вести себя так, как ожидалось. Не обязательно, чтобы reinterpret_cast<char*>(0) создавал значение нулевого указателя. Именно поэтому (char*)arg внутри функции будет проблематично и слишком поздно для преобразования. nullptr действительно единственное чистое решение здесь.

user17732522 04.09.2024 21:16

@RemyLebeau static_cast<char*>(0) должен компилироваться и вести себя точно так же, как неявное преобразование 0 в char*. reinterpret_cast<char*>(0) ведет себя иначе, чем эти два.

user17732522 04.09.2024 21:21

@МихаилТ. По сути, это сводится к разнице между char* foo = 0; и int i = 0; char* foo = i;. Поскольку NULL — это просто макрос, который расширяется до литерала 0, первое — это то, что у вас было до оболочки шаблона, а второе — то, что у вас есть после.

Miles Budnek 05.09.2024 01:30

NULL — это макрос, который можно определить как nullptr или как целочисленный литерал с нулевым значением (например, 0 или 0L). Какой из них определяется реализацией, но поскольку только последний выбор действителен в C и до C++11, шансы увидеть последний высоки.

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

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

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

Но вы передаете литерал awrap как целочисленный тип (путем вычета). Когда вы используете аргумент в func(args...), это по-прежнему целочисленное выражение со значением 0, но это не целочисленный литерал. Следовательно, аргумент не является константой нулевого указателя и не может быть неявно преобразован в char*.

Таким образом, вы не можете передать 0 в качестве параметра указателя через оболочку, поскольку вы должны указать литерал именно там, где должно применяться преобразование. А возможность использования NULL определяется реализацией. Это работает, если NULL определяется как nullptr, но не в том случае, если это целочисленный литерал с нулевым значением.

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

Это также делает C++ еще на шаг дальше от C...

Mikhail T. 04.09.2024 21:08

@МихаилТ. У C23 теперь тоже есть nullptr. Я думаю, что мой последний абзац должен относиться и к C.

user17732522 04.09.2024 21:09

Что не так с (void *)0 для C?

Mikhail T. 04.09.2024 21:30

@МихаилТ. В open-std.org/jtc1/sc22/wg14/www/docs/n3042.htm есть немало обоснований. Но он также по-прежнему страдает той же странностью, о которой я упоминал в своем ответе для C++: в C только целочисленное константное выражение значения 0 или целочисленное константное выражение значения 0, приведенное к void*, в частности, являются константами нулевого указателя, которые безопасно преобразуются в нулевой указатель ценности. В отличие от C++ неявное преобразование в типы указателей всегда возможно, но результат не может быть гарантирован.

user17732522 04.09.2024 21:41

Другие вопросы по теме

Как экспортировать ctor с ограничениями SFINAE, определенный в файле cpp, для явно созданного класса шаблона?
Как использовать Enable_if для специализации шаблона с неопределенными аргументами
Тернарный оператор не вычисляется во время компиляции — Метапрограммирование шаблонов
Выбор унифицированного типа дистрибутива из стандартной библиотеки
Аргументы функции шаблона с переменным числом аргументов не принимаются
Специализация шаблона на C++ с использованием Enable_if
Специализация шаблона C++ STD внутри другого пространства имен
Почему определение оператора = (а не объявление) должно быть написано, когда подходящий шаблон легко доступен
Использование SFINAE в конструкторе, чтобы проверить, существует ли конструктор типа члена
Использование if-constexpr и концепций для обнаружения экземпляра определенного типа расширенной политики