Я имею дело с довольно единообразным 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
Так:
для C NULL — это (void *)0;
для clang++ NULL и nullptr — одно и то же, тогда как для GNU это может быть не так...
Оборачиваемая функция — handlepath() — относится к C, и все предоставленные поставщиком примеры используют NULL.
constexpr auto ø = nullptr; теперь вы можете использовать ø.
Если вы используете современный C++, считайте, что NULL устарел. Он поддерживается только для обеспечения обратной совместимости с C. Если вы не собираетесь компилировать с C, просто используйте nullptr; это безопаснее с точки зрения непреднамеренного целочисленного приведения.
И в C, и в C++ (ссылаясь на C) NULL требуется только для расширения до константы нулевого указателя, которая представляет собой значение, которое сравнивается с нулем - тип не указан. На практике некоторые реализации определяют его как 0 или 0L или аналогичный, поэтому он имеет целочисленный тип, но другие определяют его как указательный тип (например, он расширяется до (void *)0). В C++ nullptr гарантированно имеет тип std::nullptr_t, который можно неявно преобразовать в указатель. Это важно для пакетов параметров шаблона, поскольку они не выполняют никаких неявных преобразований (например, для преобразования 0 в указатель).





Во многих реализациях 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 здесь даже не предупреждает (а тем более не ошибается)...
Проголосовал за, увидев «Во многих реализациях». Спасибо за соответствующую квалификацию.
@МихаилТ. как я объяснил, передача литерала 0 в указатель работает. Таким образом, передача NULL непосредственно в параметр-указатель исходной функции будет работать. Но ваша оболочка добавляет уровень косвенности, вы больше не передаете литерал 0 непосредственно в параметр-указатель исходной функции. Теперь вы передаете NULL в (выведенный) int параметр оболочки, а затем передаете его int в параметр-указатель исходной функции. Что не работает без приведения типов.
«(char*)NULL (он же reinterpret_cast<char*>(0))»: Это неправильно. (char*)NULL разрешится в static_cast<char*>(NULL), который использует неявное преобразование. На самом деле технически не гарантировано, что reinterpret_cast<char*>(0) будет вести себя так, как ожидалось. Не обязательно, чтобы reinterpret_cast<char*>(0) создавал значение нулевого указателя. Именно поэтому (char*)arg внутри функции будет проблематично и слишком поздно для преобразования. nullptr действительно единственное чистое решение здесь.
@RemyLebeau static_cast<char*>(0) должен компилироваться и вести себя точно так же, как неявное преобразование 0 в char*. reinterpret_cast<char*>(0) ведет себя иначе, чем эти два.
@МихаилТ. По сути, это сводится к разнице между char* foo = 0; и int i = 0; char* foo = i;. Поскольку NULL — это просто макрос, который расширяется до литерала 0, первое — это то, что у вас было до оболочки шаблона, а второе — то, что у вас есть после.
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...
@МихаилТ. У C23 теперь тоже есть nullptr. Я думаю, что мой последний абзац должен относиться и к C.
Что не так с (void *)0 для C?
@МихаилТ. В open-std.org/jtc1/sc22/wg14/www/docs/n3042.htm есть немало обоснований. Но он также по-прежнему страдает той же странностью, о которой я упоминал в своем ответе для C++: в C только целочисленное константное выражение значения 0 или целочисленное константное выражение значения 0, приведенное к void*, в частности, являются константами нулевого указателя, которые безопасно преобразуются в нулевой указатель ценности. В отличие от C++ неявное преобразование в типы указателей всегда возможно, но результат не может быть гарантирован.
NULLопределяется реализацией и может быть определен как0(для устаревших ситуаций) илиnullptr(лучше). Но, возможно, вы все равно намеревалисьnullptr? Другими словами, какова ваша мотивация вообще использоватьNULL?