Может ли компилятор привести `(void *) 0` в `execl(prog, arg, (void*) 0)` к нулевому указателю соответствующего типа?

Из интерфейса программирования Linux

execl(prog, arg, (char *) 0);
execl(prog, arg, (char *) NULL);

Обычно требуется приведение NULL к последнему вызову выше, даже в реализациях, где NULL определяется как (void *) 0.

Это связано с тем, что, хотя стандарты C требуют, чтобы нулевые указатели разных типов проверялись на равенство при проверке на равенство, они не требуют, чтобы указатели разных типов имели одинаковое внутреннее представление (хотя в большинстве реализаций так и есть).
Кроме того, в вариационной функции компилятор не может привести (void *) 0 к нулевому указателю соответствующего типа.

Стандарты C делают одно исключение из правила, согласно которому указатели разных типов не обязательно должны иметь одинаковое представление: указатели типов char * и void * должны иметь одинаковое внутреннее представление. Это означает, что передача (void *) 0 вместо (char *) 0 не будет проблемой в примере с execl(), но в общем случае требуется приведение.

  1. «Обычно требуется приведение NULL в порядке последнего вызова выше»

    Требует ли стандарт C, чтобы нулевой указатель был представлен так же, как (char*) 0?

  2. «в вариационной функции, такой как execl(), компилятор не может привести (void *) 0 к нулевому указателю соответствующего типа».

    Разве (void *) 0 не является нулевым указателем типа?

    Если да, то почему компилятор не может преобразовать (void *) 0 в execl(prog, arg, (void*) 0) в "нулевой указатель соответствующего типа"?

  3. "указатели типов char * и void * должны иметь одинаковое внутреннее представление. Это означает, что передача (void *) 0 вместо (char *) 0 не будет проблемой в примере с execl()".

    Может ли теперь компилятор привести (void *) 0 в execl(prog, arg, (void*) 0) к "нулевому указателю соответствующего типа"?

    Почему это противоречит цитате в моем пункте 2?

  4. Если я заменю (void *) 0 в execl(prog, arg, (void*) 0) на приведение 0 к указателю любого типа, например (int *) 0, сможет ли компилятор привести (int *) 0 в execl(prog, arg, (int*) 0) к "нулевому указателю соответствующего типа"? Спасибо.

  5. Может ли компилятор для невариативного вызова функции, такого как sigaction(SIGINT, &sa, (int*) 0), преобразовать (int *) 0 в "нулевой указатель соответствующего типа"?

Спасибо.


person Tim    schedule 06.09.2018    source источник
comment
прочитайте это: stackoverflow.com/questions/25381610/   -  person KPCT    schedule 06.09.2018
comment
Обратите внимание, что C — не единственный соответствующий стандарт: execl() и sigaction() определены в POSIX.1-2008 стандарт, также известный как IEEE Std 1003.1-2008. Попробуйте добавить тег posix.   -  person Nominal Animal    schedule 06.09.2018
comment
И я считаю, что POSIX требует, чтобы это работало, но я на мобильном телефоне и у меня нет цитаты под рукой.   -  person R.. GitHub STOP HELPING ICE    schedule 06.09.2018
comment
Захватывающий вопрос!! Я застрял на том же вопросе, пока читал   -  person snr    schedule 01.04.2019


Ответы (4)


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

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

Стандарт точно определяет контексты, в которых может применяться неявное преобразование; всегда должен быть целевой тип. Например, в коде int x = Y; выражение Y может быть некоторым типом, который не является int, но для которого определено неявное преобразование в int.

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

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


  1. Стандарт указывает, что значение выражения (char *)0 является нулевым указателем. Он ничего не говорит о представлении нулевых указателей, и может быть несколько разных представлений, которые все являются нулевыми указателями.

  2. Спецификация функции execl говорит, что список аргументов должен заканчиваться (char *)0, который является значением типа char *. Значение типа void * не является значением типа char *, и в этом контексте нет неявных преобразований, как обсуждалось выше.

  3. Неявного преобразования по-прежнему нет; текст, который вы цитируете, говорит, что вы можете использовать аргумент неправильного типа в этой конкретной ситуации (нет параметра прототипа; и char * ожидается, но void * предоставлено, или наоборот).

  4. Это было бы неопределенным поведением, текст, который вы процитировали в пункте 3, не относится к int *.

  5. У функции sigaction есть прототип; рассматриваемый параметр - struct sigaction *oldact. Когда вы пытаетесь инициализировать параметр прототипа (или любую переменную) со значением другого типа, предпринимается попытка неявного преобразования к типу параметра. Существует неявное преобразование любого значения нулевого указателя в значение нулевого указателя другого типа. Это правило находится в C11 6.3.2.3/4. Так что этот код подходит.

person M.M    schedule 06.09.2018
comment
@EricPostpischil У них разные типы, несмотря на то, что они взаимозаменяемы в качестве аргументов функций ... Вопрос 2 OP не очень ясен, я попытаюсь перефразировать свой ответ на эту часть - person M.M; 06.09.2018
comment
@AndrewHenle, вы путаете константу нулевого указателя с нулевым указателем. (char *)0 является нулевым указателем и константой адреса, но не константой нулевого указателя ! - person M.M; 07.09.2018
comment
Это моя точка зрения - (char *)0 не константа нулевого указателя. Начинается вторая часть 6.3.2.3p3. Если константа нулевого указателя преобразуется в тип указателя... Поскольку (char *)0 не является константой нулевого указателя, как она становится нулевым указателем? Я бы сказал, что это не строго совместимый способ получения нулевого указателя, хотя он, вероятно, сработает. Хотя я открыт для интерпретаций того, что на самом деле это нулевой указатель, поскольку цель этой части стандарта C состояла в том, чтобы проверить плохой код, который предполагал, что 0 является нулевым указателем. - person Andrew Henle; 07.09.2018
comment
@AndrewHenle Я никогда не утверждал, что (char *)0 является константой нулевого указателя. Второе предложение 6.3.2.3/3 объясняет, почему это нулевой указатель. Константа нулевого указателя 0 преобразуется в тип указателя, который создает нулевой указатель. - person M.M; 07.09.2018
comment
Ваша интерпретация требует, чтобы приведение было преобразованием. Если это так, то или подобное выражение, приведенное к оператору типа void * в 6.3.2.3p3, является посторонним, и я бы сказал, что оно вводит в заблуждение. Почему стандарт выделяет приведение к void *, если любое приведение целочисленного нулевого константного выражения к указателю будет допустимым нулевым указателем? Я вижу, что что-то вроде ( int * ) 0 является допустимым ненулевым указателем в таких архитектурах, как некоторые модели памяти x86 или целочисленные указатели Cray на основе слов (IIRC). Следует признать, что приведение (char *) может быть особенным, поскольку оно имеет много общего с void *. - person Andrew Henle; 07.09.2018
comment
@AndrewHenle Приведение — это явное преобразование. Операнд оператора приведения преобразуется в тип, указанный в скобках. (С11 6.5.4/5). Текст, который вы цитируете, не является посторонним, вы снова, похоже, путаете константу нулевого указателя с нулевым указателем. (void *)0 — это нулевой указатель и константа нулевого указателя. (char *)0 — это нулевой указатель, а не константа нулевого указателя. - person M.M; 07.09.2018

Начиная с C99, спецификация va_arg читается частично

Если [тип, переданный в va_arg в качестве аргумента] несовместим с типом фактического следующего аргумента (который продвигается в соответствии с продвижением аргумента по умолчанию), поведение не определено, за исключением следующих случаев:

  • один тип — целочисленный тип со знаком, другой тип — соответствующий целочисленный тип без знака, и значение может быть представлено в обоих типах;
  • один тип является указателем на void, а другой — указателем на символьный тип.

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

variadic_function("a", "b", "c", (void *)0);

будет действительным всякий раз, когда

variadic_function("a", "b", "c", (char *)0);

было бы.

К сожалению, есть одна загвоздка: я не могу найти никаких требований к вариативным функциям стандартной библиотеки1, чтобы [вести себя так, как если бы они] обращались к своим аргументам, создавая серию звонков на va_arg. Вы, наверное, думаете, ну а как еще они собираются это сделать? На практике это va_arg или написанный от руки язык ассемблера, и, возможно, комитет не хотел требовать, чтобы написанный от руки язык ассемблера был совершенно эквивалентен, но я бы не стал об этом беспокоиться.

Так что книга, которую вы цитируете, технически неверна. Но я бы все равно написал

execl(prog, arg, (char *) NULL);

если бы я собирался использовать NULL в первую очередь (обычно я предпочитаю использовать 0 в качестве константы нулевого указателя), потому что вы не должны писать код, который полагается на NULL, расширяющийся до ((void *)0), и

execl(prog, arg, 0);

безусловно неверно. Например, execl не получит нулевой указатель от этого 0 ни в одном ABI, где int — 32 бита, char * — 64 бита, а int величины не расширяются до 64 битов со знаком или нулем при передаче в составе списка переменных аргументов. .


1 execl не является частью стандарта C, но является частью стандарта POSIX, и любая система, предоставляющая execl в первую очередь, вероятно, совместима по крайней мере с подмножеством POSIX. Все пункт 7.1.4 стандарта C могут быть Предполагается, что они применяются и к функциям, указанным в POSIX.

person zwol    schedule 06.09.2018

1) Требует ли стандарт C представления нулевого указателя так же, как (char*) 0

Да, поскольку константа нулевого указателя имеет тип void *, а void * и char * имеют одинаковое представление.

Это подробно описано в разделе 6.3.2.3p3 документа C. стандарт:

Целочисленное константное выражение со значением 0 или такое выражение, приведенное к типу void * , называется константой нулевого указателя.

И раздел 6.2.5п28:

Указатель на void должен иметь те же требования к представлению и выравниванию, что и указатель на символьный тип. 48)

...

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


2) Разве (void *) 0 не является нулевым указателем типа? Если да, то почему компилятор не может преобразовать (void *) 0 в execl(prog, arg, (void*) 0) в "нулевой указатель соответствующего типа"?

Это нулевой указатель типа, и этот тип — void *.

Определение execl:

int execl(const char *path, const char *arg, ...);

Таким образом, он не может привести третий параметр к соответствующему типу, потому что он не знает, какой это подходящий тип, но это не имеет значения, поскольку void * и char * взаимозаменяемы в соответствии с 6.2.4p28 и сноской 48, как упоминалось выше.

3) Может ли теперь компилятор преобразовать (void *) 0 в execl(prog, arg, (void*) 0) в "нулевой указатель соответствующего типа"? Почему это противоречит цитате в моем пункте 2?

Он по-прежнему не может выполнить приведение, потому что не знает, какой тип является подходящим. Но опять же, это не имеет значения, потому что void * и char * взаимозаменяемы.

4) Если я заменю (void *) 0 в execl(prog, arg, (void*) 0) на приведение 0 к указателю любого типа, например (int *) 0, сможет ли компилятор преобразовать (int *) 0 в execl(prog, arg, (int*) 0) к "нулевому указателю соответствующего типа"?

Нет, потому что опять же он не знает, какой тип подходит. И в этом случае у вас может возникнуть проблема, если int * и char * не имеют одинакового представления.

5) Может ли компилятор для невариативного вызова функции, такого как sigaction(SIGINT, &sa, (int*) 0), привести (int *) 0 к "нулевому указателю соответствующего типа"?

Да, потому что (int *)0 — это нулевой указатель, и потому что нулевой указатель можно преобразовать в любой другой указатель.

person dbush    schedule 06.09.2018
comment
поскольку константа нулевого указателя имеет тип void *, - 0 также является константой нулевого указателя с типом int - person M.M; 06.09.2018
comment
@EricPostpischil IMO C11 6.2.5p28 (со сноской 48) пытается сказать, что char * и void * могут использоваться взаимозаменяемо в этой ситуации. Я не могу придумать никакого другого возможного значения или интерпретации этого предложения. - person M.M; 06.09.2018
comment
@EricPostpischil лично я буду придерживаться намерений, выраженных в 6.2.5p28, в конце концов, цель стандарта состоит в том, чтобы кодифицировать предполагаемый набор правил, и я думаю, что ясно, что здесь предполагалась взаимозаменяемость, даже если формулировка 6.5 .2.2/6 кажется противоречивым. - person M.M; 06.09.2018
comment
Его не было в C89, но в C99 и C11 определение va_arg содержит предложение, специально разрешающее va_arg (ap, char *), когда вызывающий объект передал void *, и наоборот (N1570: 7.16.1.1p2, второй пункт). Мне этого достаточно, чтобы поддержать интерпретацию М.М. Однако я бы сказал, что в этом контексте следует писать (char *)0 или (char *)NULL просто потому, что никогда не следует полагаться на то, что NULL имеет конкретное определение. - person zwol; 06.09.2018
comment
Да, потому что (int *)0 — нулевой указатель Так ли это? Согласно 6.3.2.3, p3: целочисленная константа. выражение со значением 0 или такое выражение, приведенное к типу void *, называется константой нулевого указателя. ... (int *)0 не (void *)0. - person Andrew Henle; 06.09.2018

AFAI понимает, что продвижение аргумента по умолчанию применяется к функции var-args, за исключением указателей, которые остались прежними (пример zwol также поддерживает 1). Поэтому, когда 0 передается функциям типа var-args, например. семейство exec(), оно распознается как неукрашенное целое число 0 вместо нулевого указателя. Между прочим, execl(prog, arg, 0); может работать в системах, где внутреннее представление нулевого указателя и целое число 0 совпадают, но это не обязательно.

execl(prog, arg, NULL); также может непреднамеренно работать в соответствии с одним из следующих

  • Если NULL определено в системе как целое число 0, применяется вышеупомянутое объяснение.
  • Если NULL определено в системе как константа нулевого указателя, как (void*)0, хотя в вариационной функции компилятор не может привести (void*)0 к нулевому указателю соответствующего типа, даже если стандарты C говорят char* и void* должны иметь одинаковое внутреннее представление.

Для получения дополнительной информации посетите здесь. Дополнительный пример из здесь.


1 Например, execl не получит нулевой указатель от этого 0 ни в одном ABI, где int — 32 бита, char * — 64 бита, а int величины не расширяются до 64 битов со знаком или нулем, когда передается как часть списка переменных аргументов.

person snr    schedule 01.04.2019