[C++, Программирование, Производство и разработка электроники, Разработка под Linux] Как можно и как нельзя использовать нулевой указатель в С++
Автор
Сообщение
news_bot ®
Стаж: 6 лет 9 месяцев
Сообщений: 27286
Некоторым этот банальный вопрос уже набил оскомину, но мы взяли 7 примеров и попытались объяснить их поведение при помощи стандарта:
struct A {
int data_mem;
void non_static_mem_fn() {}
static void static_mem_fn() {}
};
void foo(int) {}
A* p{nullptr};
/*1*/ *p;
/*2*/ foo((*p, 5));
/*3*/ A a{*p};
/*4*/ p->data_mem;
/*5*/ int b{p->data_mem};
/*6*/ p->non_static_mem_fn();
/*7*/ p->static_mem_fn();
Очевидная, но важная деталь: p, инициализированный нулевым указателем, не может указывать на объект типа А, потому что его значение отлично от значения любого указателя на объект типа А conv.ptr#1.
Disclaimer: статья содержит вольный перевод терминов и выдержек из стандарта на русский язык. Мы рекомендуем английскую версию статьи на dev.to, лишенную неточностей перевода.
Пример 1
Открыть начало кода
SPL
struct A {
int data_mem;
void non_static_mem_fn() {}
static void static_mem_fn() {}
};
void foo(int) {}
A* p{nullptr};
*p;
Синтаксически это оператор выражения (expression statement, stmt.expr#1), в котором *p является выражением с отброшенным результатом, который, тем не менее, нужно вычислить. Определение унарного оператора * expr.unary.op#1 гласит, что этот оператор осуществляет косвенное обращение (indirection), и результатом является l-значение, которое обозначает объект или функцию, на которую указывает выражение. Его семантика понятна, чего не скажешь о том, должен ли объект существовать. Нулевой указатель в определении не упоминается ни разу.
Можно попробовать зацепиться за косвенное обращение, потому что есть basic.stc#4, в котором четко написано, что поведение при косвенном обращении через недопустимое значение указателя (indirection through an invalid pointer value) не определено. Но там же дается описание недопустимого значения указателя, под которое нулевой не подходит, и дается ссылка на basic.compound#3.4, где видно, что нулевой указатель и недопустимый — это различные значения указателя.
Еще есть примечание в dcl.ref#5, которое гласит, что «the only way to create such a reference would be to bind it to the “object” obtained by indirection through a null pointer, which causes undefined behavior», т.е. единственный способ создать такую ссылку — привязать ее к «объекту», полученному за счет косвенного обращения через нулевой указатель, что приводит к неопределенному поведению. Но придаточное в конце может относиться не только к косвенному обращению, но и к «привязать» (to bind), и в этом случае неопределенное поведение вызвано тем, что нулевой указатель не указывает на объект, о чем и говорится в основном тексте пункта dcl.ref#5.
Раз стандарт вместо однозначных формулировок оставляет пространство для интерпретаций в разрезе нашего вопроса, можно обратиться к списку дефектов языковой части стандарта, где Core Working Group среди прочего поясняет текст стандарта. Наш вопрос выделен в отдельный дефект, где CWG довольно давно пришла к неформальному консенсусу (так определен статус drafting), что неопределенное поведение влечет не разыменование само по себе, а конвертация результата разыменования из l-значения в r-значение. Если «неформальный консенсус CWG» звучит недостаточно весомо, то есть другой дефект, в котором рассматривается пример, аналогичный нашему примеру 7. Такой код назван корректным по этой же причине в официальной аргументации CWG.
В дальнейших рассуждениях мы будем опираться на этот консенсус. Если в будущем стандарт запретит разыменовывать нулевые указатели по примеру Си (N2176, 6.5.3.2 и сноска 104), значит, все примеры содержат неопределенное поведение, и на этом разговор можно закончить.
Пример 2
Открыть начало кода
SPL
struct A {
int data_mem;
void non_static_mem_fn() {}
static void static_mem_fn() {}
};
void foo(int) {}
A* p{nullptr};
foo((*p, 5));
Чтобы вызвать foo, требуется проинициализировать его параметр, для чего нужно вычислить результат оператора «запятая». Его операнды вычисляются слева направо, причем все, кроме последнего, являются выражениями с отброшенным значением так же, как и в примере 1 (expr.comma#1). Следовательно, этот пример также корректен.
Пример 3
Открыть начало кода
SPL
struct A {
int data_mem;
void non_static_mem_fn() {}
static void static_mem_fn() {}
};
void foo(int) {}
A* p{nullptr};
A a{*p};
Для инициализации a будет выбран неявный конструктор копирования, и для того, чтобы его вызвать, нужно проинициализировать параметр const A& допустимым объектом, в противном случае поведение не определено (dcl.ref#5). В нашем случае допустимого объекта нет.
Пример 4
Открыть начало кода
SPL
struct A {
int data_mem;
void non_static_mem_fn() {}
static void static_mem_fn() {}
};
void foo(int) {}
A* p{nullptr};
p->data_mem;
Выражение этого оператора выражения при вычислении будет раскрыто в (*(p)).data_mem согласно expr.ref#2, которое обозначает (designate) соответствующий подобъект объекта, на который указывает выражение до точки (expr.ref#6.2). Параллели с примером 1 становятся особенно явными, если открыть, скажем, basic.lookup.qual#1, и увидеть, как to refer и to designate взаимозаменяемо используются в том же смысле, что и в expr.ref. Из чего мы делаем вывод, что это корректный код, однако некоторые компиляторы не согласны (см. про проверку константными выражениями в конце статьи).
Пример 5
Открыть начало кода
SPL
struct A {
int data_mem;
void non_static_mem_fn() {}
static void static_mem_fn() {}
};
void foo(int) {}
A* p{nullptr};
int b{p->data_mem};
В продолжение предыдущего примера не будем отбрасывать результат, а проинициализируем им int. В этом случае результат нужно конвертировать в pr-значение, потому что выражения именно этой категории инициализируют объекты (basic.lval#1.2). Так как речь идет об int, будет осуществлен доступ к объекту результата (conv.lval#3.4), что в нашем случае ведет к неопределенному поведению, потому что ни одно из условий basic.lval#11 не соблюдается.
Пример 6
Открыть начало кода
SPL
struct A {
int data_mem;
void non_static_mem_fn() {}
static void static_mem_fn() {}
};
void foo(int) {}
A* p{nullptr};
p->non_static_mem_fn();
class.mfct.non-static#1 гласит, что функции-члены разрешено вызывать для объекта типа, к которому они принадлежат (или унаследованного от него), или напрямую из определений функций-членов класса. Именно «разрешено» — такой смысл вкладывается в глагол «may be» в директивах ИСО/МЭК, которым следуют все стандарты ИСО. Раз объекта нет, то и поведение при таком вызове не определено.
Пример 7
Открыть начало кода
SPL
struct A {
int data_mem;
void non_static_mem_fn() {}
static void static_mem_fn() {}
};
void foo(int) {}
A* p{nullptr};
p->static_mem_fn();
Как говорилось в рассуждениях к примеру 1, Core Working Group считает этот код корректным. Добавить можно лишь то, что согласно сноске 59, выражение слева от оператора -> разыменовывается, даже его результат не требуется.
Проверка с помощью constexpr
Раз константные выражения не могут полагаться на неопределенное поведение (expr.const#5), то можно узнать мнение компиляторов о наших примерах. Пусть они и несовершенны, но как минимум нередко правы. Мы взяли три популярных компилятора, подправили пример под constexpr и для наглядности закомментировали те примеры, которые не компилируются, потому что сообщения об ошибках что у GCC, что у MSVC оставляют желать лучшего на данных примерах: godbolt.
Что получилось в итоге:
#
Код
Предположение
GCC 10.1
Clang 10
MSVC 19.24
1
*p;
+
+
+
+
2
foo((*p, 5));
+
+
+
+
3
A a{*p};
4
p->data_mem;
+
+
5
int b{p->data_mem};
6
p->non_static_mem_fn();
+
+
7
p->static_mem_fn();
+
+
+
+
Результаты заставляют несколько усомниться в выводе из примера 6 и в большей степени из примера 4. Но также интересно, что мы все сходимся во мнении о ключевом примере 1.
Спасибо, что остались с нами до конца, чтобы проследить за приключениями нулевого указателя в С++! :-) Обычно мы делимся на Хабре кусками кода из реальных проектов по разработке встроенного ПО для электроники, но этот раз нас заинтересовали чисто «философские» вопросы, поэтому примеры синтетические.
Если вы разделяете нашу любовь к противоречиям в С++, делитесь «наболевшим» в комментариях.
===========
Источник:
habr.com
===========
Похожие новости:
- [Dart, Flutter, Программирование, Разработка мобильных приложений] Flutter. RenderObject — замеряй и властвуй
- [Информационная безопасность, Программирование, Разработка под Linux] Побег из привилегированных Docker-контейнеров (перевод)
- [FPGA, Высокая производительность, Параллельное программирование, Программирование микроконтроллеров, Электроника для начинающих] Как начать путь к работе по проектированию электроники FPGA космического корабля Blue Origin
- [Java, Программирование, Профессиональная литература] Фреймворк Quarkus: как в нем реализуется чистая архитектура (перевод)
- [Java, PostgreSQL, Программирование, Тестирование IT-систем] Работа с базами данных глазами разработчика
- [DIY или Сделай сам, Программирование микроконтроллеров, Производство и разработка электроники, Разработка игр, Схемотехника] Doom Boy ESP32. Вторая итерация
- [Java, Microsoft Azure, Программирование] Использование Azure Service Bus из Java
- [Java, MongoDB, Программирование] The Testcontainers’ MongoDB Module and Spring Data MongoDB in Action
- [Системное программирование, Программирование микроконтроллеров, Компьютерное железо] Моделируем поведение Quartus-проекта на Verilog в среде ModelSim
- [Производство и разработка электроники] Снижение стоимости развертывания частной сети LoRaWAN
Теги для поиска: #_c++, #_programmirovanie (Программирование), #_proizvodstvo_i_razrabotka_elektroniki (Производство и разработка электроники), #_razrabotka_pod_linux (Разработка под Linux), #_s++ (С++), #_null, #_gcc, #_clang, #_msvc, #_nulevye_ukazateli (нулевые указатели), #_cpp, #_promwad, #_programmirovanie (программирование), #_c++, #_programmirovanie (
Программирование
), #_proizvodstvo_i_razrabotka_elektroniki (
Производство и разработка электроники
), #_razrabotka_pod_linux (
Разработка под Linux
)
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 16:51
Часовой пояс: UTC + 5
Автор | Сообщение |
---|---|
news_bot ®
Стаж: 6 лет 9 месяцев |
|
Некоторым этот банальный вопрос уже набил оскомину, но мы взяли 7 примеров и попытались объяснить их поведение при помощи стандарта: struct A {
int data_mem; void non_static_mem_fn() {} static void static_mem_fn() {} }; void foo(int) {} A* p{nullptr}; /*1*/ *p; /*2*/ foo((*p, 5)); /*3*/ A a{*p}; /*4*/ p->data_mem; /*5*/ int b{p->data_mem}; /*6*/ p->non_static_mem_fn(); /*7*/ p->static_mem_fn(); Очевидная, но важная деталь: p, инициализированный нулевым указателем, не может указывать на объект типа А, потому что его значение отлично от значения любого указателя на объект типа А conv.ptr#1. Disclaimer: статья содержит вольный перевод терминов и выдержек из стандарта на русский язык. Мы рекомендуем английскую версию статьи на dev.to, лишенную неточностей перевода. Пример 1 Открыть начало кодаSPLstruct A {
int data_mem; void non_static_mem_fn() {} static void static_mem_fn() {} }; void foo(int) {} A* p{nullptr}; *p;
Синтаксически это оператор выражения (expression statement, stmt.expr#1), в котором *p является выражением с отброшенным результатом, который, тем не менее, нужно вычислить. Определение унарного оператора * expr.unary.op#1 гласит, что этот оператор осуществляет косвенное обращение (indirection), и результатом является l-значение, которое обозначает объект или функцию, на которую указывает выражение. Его семантика понятна, чего не скажешь о том, должен ли объект существовать. Нулевой указатель в определении не упоминается ни разу. Можно попробовать зацепиться за косвенное обращение, потому что есть basic.stc#4, в котором четко написано, что поведение при косвенном обращении через недопустимое значение указателя (indirection through an invalid pointer value) не определено. Но там же дается описание недопустимого значения указателя, под которое нулевой не подходит, и дается ссылка на basic.compound#3.4, где видно, что нулевой указатель и недопустимый — это различные значения указателя. Еще есть примечание в dcl.ref#5, которое гласит, что «the only way to create such a reference would be to bind it to the “object” obtained by indirection through a null pointer, which causes undefined behavior», т.е. единственный способ создать такую ссылку — привязать ее к «объекту», полученному за счет косвенного обращения через нулевой указатель, что приводит к неопределенному поведению. Но придаточное в конце может относиться не только к косвенному обращению, но и к «привязать» (to bind), и в этом случае неопределенное поведение вызвано тем, что нулевой указатель не указывает на объект, о чем и говорится в основном тексте пункта dcl.ref#5. Раз стандарт вместо однозначных формулировок оставляет пространство для интерпретаций в разрезе нашего вопроса, можно обратиться к списку дефектов языковой части стандарта, где Core Working Group среди прочего поясняет текст стандарта. Наш вопрос выделен в отдельный дефект, где CWG довольно давно пришла к неформальному консенсусу (так определен статус drafting), что неопределенное поведение влечет не разыменование само по себе, а конвертация результата разыменования из l-значения в r-значение. Если «неформальный консенсус CWG» звучит недостаточно весомо, то есть другой дефект, в котором рассматривается пример, аналогичный нашему примеру 7. Такой код назван корректным по этой же причине в официальной аргументации CWG. В дальнейших рассуждениях мы будем опираться на этот консенсус. Если в будущем стандарт запретит разыменовывать нулевые указатели по примеру Си (N2176, 6.5.3.2 и сноска 104), значит, все примеры содержат неопределенное поведение, и на этом разговор можно закончить. Пример 2 Открыть начало кодаSPLstruct A {
int data_mem; void non_static_mem_fn() {} static void static_mem_fn() {} }; void foo(int) {} A* p{nullptr}; foo((*p, 5));
Чтобы вызвать foo, требуется проинициализировать его параметр, для чего нужно вычислить результат оператора «запятая». Его операнды вычисляются слева направо, причем все, кроме последнего, являются выражениями с отброшенным значением так же, как и в примере 1 (expr.comma#1). Следовательно, этот пример также корректен. Пример 3 Открыть начало кодаSPLstruct A {
int data_mem; void non_static_mem_fn() {} static void static_mem_fn() {} }; void foo(int) {} A* p{nullptr}; A a{*p};
Для инициализации a будет выбран неявный конструктор копирования, и для того, чтобы его вызвать, нужно проинициализировать параметр const A& допустимым объектом, в противном случае поведение не определено (dcl.ref#5). В нашем случае допустимого объекта нет. Пример 4 Открыть начало кодаSPLstruct A {
int data_mem; void non_static_mem_fn() {} static void static_mem_fn() {} }; void foo(int) {} A* p{nullptr}; p->data_mem;
Выражение этого оператора выражения при вычислении будет раскрыто в (*(p)).data_mem согласно expr.ref#2, которое обозначает (designate) соответствующий подобъект объекта, на который указывает выражение до точки (expr.ref#6.2). Параллели с примером 1 становятся особенно явными, если открыть, скажем, basic.lookup.qual#1, и увидеть, как to refer и to designate взаимозаменяемо используются в том же смысле, что и в expr.ref. Из чего мы делаем вывод, что это корректный код, однако некоторые компиляторы не согласны (см. про проверку константными выражениями в конце статьи). Пример 5 Открыть начало кодаSPLstruct A {
int data_mem; void non_static_mem_fn() {} static void static_mem_fn() {} }; void foo(int) {} A* p{nullptr}; int b{p->data_mem};
В продолжение предыдущего примера не будем отбрасывать результат, а проинициализируем им int. В этом случае результат нужно конвертировать в pr-значение, потому что выражения именно этой категории инициализируют объекты (basic.lval#1.2). Так как речь идет об int, будет осуществлен доступ к объекту результата (conv.lval#3.4), что в нашем случае ведет к неопределенному поведению, потому что ни одно из условий basic.lval#11 не соблюдается. Пример 6 Открыть начало кодаSPLstruct A {
int data_mem; void non_static_mem_fn() {} static void static_mem_fn() {} }; void foo(int) {} A* p{nullptr}; p->non_static_mem_fn();
class.mfct.non-static#1 гласит, что функции-члены разрешено вызывать для объекта типа, к которому они принадлежат (или унаследованного от него), или напрямую из определений функций-членов класса. Именно «разрешено» — такой смысл вкладывается в глагол «may be» в директивах ИСО/МЭК, которым следуют все стандарты ИСО. Раз объекта нет, то и поведение при таком вызове не определено. Пример 7 Открыть начало кодаSPLstruct A {
int data_mem; void non_static_mem_fn() {} static void static_mem_fn() {} }; void foo(int) {} A* p{nullptr}; p->static_mem_fn();
Как говорилось в рассуждениях к примеру 1, Core Working Group считает этот код корректным. Добавить можно лишь то, что согласно сноске 59, выражение слева от оператора -> разыменовывается, даже его результат не требуется. Проверка с помощью constexpr Раз константные выражения не могут полагаться на неопределенное поведение (expr.const#5), то можно узнать мнение компиляторов о наших примерах. Пусть они и несовершенны, но как минимум нередко правы. Мы взяли три популярных компилятора, подправили пример под constexpr и для наглядности закомментировали те примеры, которые не компилируются, потому что сообщения об ошибках что у GCC, что у MSVC оставляют желать лучшего на данных примерах: godbolt. Что получилось в итоге: # Код Предположение GCC 10.1 Clang 10 MSVC 19.24 1 *p; + + + + 2 foo((*p, 5)); + + + + 3 A a{*p}; 4 p->data_mem; + + 5 int b{p->data_mem}; 6 p->non_static_mem_fn(); + + 7 p->static_mem_fn(); + + + + Результаты заставляют несколько усомниться в выводе из примера 6 и в большей степени из примера 4. Но также интересно, что мы все сходимся во мнении о ключевом примере 1. Спасибо, что остались с нами до конца, чтобы проследить за приключениями нулевого указателя в С++! :-) Обычно мы делимся на Хабре кусками кода из реальных проектов по разработке встроенного ПО для электроники, но этот раз нас заинтересовали чисто «философские» вопросы, поэтому примеры синтетические. Если вы разделяете нашу любовь к противоречиям в С++, делитесь «наболевшим» в комментариях. =========== Источник: habr.com =========== Похожие новости:
Программирование ), #_proizvodstvo_i_razrabotka_elektroniki ( Производство и разработка электроники ), #_razrabotka_pod_linux ( Разработка под Linux ) |
|
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 16:51
Часовой пояс: UTC + 5