Я имею дело с довольно единообразным 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
?