С Новым годом! Форум программистов, компьютерный форум, киберфорум
C++
Войти
Регистрация
Восстановить пароль
Блоги Сообщество Поиск Заказать работу  
 
 
Рейтинг 4.76/25: Рейтинг темы: голосов - 25, средняя оценка - 4.76
Вездепух
Эксперт CЭксперт С++
 Аватар для TheCalligrapher
12930 / 6798 / 1820
Регистрация: 18.10.2014
Сообщений: 17,205

Головоломка для тех, кому под Новый Год нечего делать

28.12.2023, 09:07. Показов 5303. Ответов 34
Метки нет (Все метки)

Студворк — интернет-сервис помощи студентам
Тема на самом деле уже не раз упоминалась, но тем не менее... Пример кода

C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <iostream>
 
struct S
{
  void*& r;
  S(void*& r) : r(r) {}
  ~S() { r = nullptr; }
};
 
void* foo(S s)
{
  return s.r;
}
 
int main()
{
  void* p = &p;
  p = foo(p);
  std::cout << p << std::endl;
}
После компиляции под Clang/GCC этот код печатает нулевой указатель. После компиляции под MSVC++ код печатает ненулевой указатель (т.е. исходное значение p).

Головоломка: кто прав согласно спецификации языка?
0
IT_Exp
Эксперт
34794 / 4073 / 2104
Регистрация: 17.06.2006
Сообщений: 32,602
Блог
28.12.2023, 09:07
Ответы с готовыми решениями:

Кому скучно и нечего делать, у меня есть нерабочий код
проблема нетривиальная. Вообщем я вроде всё в коде подписал. привожу пример того как должен работать класс на словах: Мы взяли число 73,...

Если кому-то нечего делать по выходным в ближайшие полтора года
Просто делюсь интересной ссылкой) http://www.cppgm.org/

Крестики-нолики. Кому делать нечего научите как исправить ошибку
Возникает при результате ничья. File &quot;6tick-tack-toe.py&quot;, line 212, in &lt;module&gt; main() File &quot;6tick-tack-toe.py&quot;, line...

34
фрилансер
 Аватар для Алексей1153
6447 / 5643 / 1128
Регистрация: 11.10.2019
Сообщений: 15,017
28.12.2023, 09:31
Цитата Сообщение от TheCalligrapher Посмотреть сообщение
void* p = &p;
инициализируется указателем на самого себя ?

интересный сюжет

Добавлено через 1 минуту
Цитата Сообщение от TheCalligrapher Посмотреть сообщение
p = foo(p);
то есть, тут вопрос, что произойдёт быстрее - деструктор временного объекта типа S или оператор присваивания

я так понимаю, деструктор должен отработать позже, так что неправы вот эти
Clang/GCC
Добавлено через 1 минуту
а, хотя нет, всё как раз наоборот - сначала присваивание, а потом деструктор по ссылке зануляет указатель. Значит, студия не права
0
Вездепух
Эксперт CЭксперт С++
 Аватар для TheCalligrapher
12930 / 6798 / 1820
Регистрация: 18.10.2014
Сообщений: 17,205
28.12.2023, 09:34  [ТС]
Цитата Сообщение от Алексей1153 Посмотреть сообщение
инициализируется указателем на самого себя ?
Это не принципиально. Просто нужно было инициализировать указатель чем-нибудь ненулевым...

Цитата Сообщение от Алексей1153 Посмотреть сообщение
то есть, тут вопрос, что произойдёт быстрее - деструктор временного объекта типа S или оператор присваивания
Верно. Суть вопроса именно в этом.

Однако, ключевой момент тут как раз в том, что та сущность типа S, которая тут создается - это не временный объект. Если бы параметр функции имел тип const S &s, тогда бы тут действительно создавался временный объект. И, кстати, поведение всех компиляторов было бы одинаковым. Однако в исходном варианте это не временный объект.
0
фрилансер
 Аватар для Алексей1153
6447 / 5643 / 1128
Регистрация: 11.10.2019
Сообщений: 15,017
28.12.2023, 09:40
а, то есть, это же S s - локальная переменная внутри функции

но возвращается копия указателя. Затем отрабатывает деструктор, после чего копия должна присвоиться p. И он должен быть таки не нулевой , хм
0
Вездепух
Эксперт CЭксперт С++
 Аватар для TheCalligrapher
12930 / 6798 / 1820
Регистрация: 18.10.2014
Сообщений: 17,205
28.12.2023, 20:43  [ТС]
Цитата Сообщение от Алексей1153 Посмотреть сообщение
а, то есть, это же S s - локальная переменная внутри функции
У вас сначала недолет ("временный объект"), а затем перелет ("локальная переменная").

Параметр функции похож на "локальную переменную" только с точки зрения области видимости идентификатора. Во всех остальных отношениях параметр ведет себя по особому, совсем не как "локальная переменная". Параметры - это не локальные переменные.
0
901 / 478 / 93
Регистрация: 10.06.2014
Сообщений: 2,700
28.12.2023, 21:39
Цитата Сообщение от TheCalligrapher Посмотреть сообщение
Головоломка: кто прав согласно спецификации языка?
Предположу, что правы оба. Объясню ответ.

По идее к возвращаемому значению из функции foo может быть применена оптимизация copy elision, а может и нет. Так как может, но необязательно, поэтому каждый из компиляторов может поступать по своему, и каждый будет прав по своему...
0
Вездепух
Эксперт CЭксперт С++
 Аватар для TheCalligrapher
12930 / 6798 / 1820
Регистрация: 18.10.2014
Сообщений: 17,205
28.12.2023, 21:59  [ТС]
Цитата Сообщение от Undisputed Посмотреть сообщение
По идее к возвращаемому значению из функции foo может быть применена оптимизация copy elision, а может и нет.
Не понял. Как на поведение этого кода может повлиять copy elision на возвращаемом значении? Возвращаемое значение тут имеет тип void *. Это вообще обычный скаляр... Как copy elision может повлиять на возвращение скаляра?
0
901 / 478 / 93
Регистрация: 10.06.2014
Сообщений: 2,700
28.12.2023, 22:27
TheCalligrapher,
Наверное правильнее было сказать, что "нет необходимости создавать объект S", так как мы возвращаем значение его поля, которое известно еще до вызова самой функции. Другими словами, функцию foo можно "перефразировать" так:
C++
1
2
3
4
void* foo(void* r)
{
  return r;
}
Что эквивалентно этому:
C++
1
2
//p = foo(p);
p = p;
но оптимизировать или нет, решать компилятору... это именно то, что я имел ввиду (думаю зря применил термин copy elision, который имеет вполне конкретное значение в рамках языка)

Добавлено через 55 секунд
если будет оптимизация, то получим адрес, если нет, то nullptr
0
Вездепух
Эксперт CЭксперт С++
 Аватар для TheCalligrapher
12930 / 6798 / 1820
Регистрация: 18.10.2014
Сообщений: 17,205
28.12.2023, 22:36  [ТС]
Цитата Сообщение от Undisputed Посмотреть сообщение
Наверное правильнее было сказать, что "нет необходимости создавать объект S"
Но это уже будет не copy elision. И в такой ситуации компилятору не позволяется игнорировать побочные эффекты конструктора и деструктора класса S. То есть что бы тут ни оптимизировал компилятор, игнорировать деструктор S он не имеет права.

Поэтому вопрос фактически сводится к: когда, согласно спецификации языка, должен отработать деструктор S?
0
901 / 478 / 93
Регистрация: 10.06.2014
Сообщений: 2,700
28.12.2023, 22:48
Цитата Сообщение от TheCalligrapher Посмотреть сообщение
Но это уже будет не copy elision.
Да, согласен

Цитата Сообщение от TheCalligrapher Посмотреть сообщение
То есть что бы тут ни оптимизировал компилятор, игнорировать деструктор S он не имеет права.
Если так, то деструктор в любом случае должен быть вызван, а значит присвоение nullptr по ссылке в любом случае должно произойти. По идее когда мы уже дойдем до вывода значения p, все эффекты от деструктора должны быть завершены. Поэтому чисто логически, должно быть nullptr.

Но думаю это слишком очевидно, скорее всего есть подвох (если будет время/желание, поищу ответ, интересно)...
0
Вездепух
Эксперт CЭксперт С++
 Аватар для TheCalligrapher
12930 / 6798 / 1820
Регистрация: 18.10.2014
Сообщений: 17,205
28.12.2023, 23:43  [ТС]
Цитата Сообщение от Undisputed Посмотреть сообщение
Если так, то деструктор в любом случае должен быть вызван, а значит присвоение nullptr по ссылке в любом случае должно произойти. По идее когда мы уже дойдем до вывода значения p, все эффекты от деструктора должны быть завершены.
Верно.

Цитата Сообщение от Undisputed Посмотреть сообщение
Поэтому чисто логически, должно быть nullptr.
Ну почему же? Как уже правильно заметил Алексей1153 выше, вполне может быть, что деструктор выполнит свое присваивание еще до того, как выполнится присваивание p = <результат вызова функции>. Если это легально, то nullptr в p проживет лишь миг, а к моменту вывода в p уже вернется ненулевое значение.
0
27 / 24 / 4
Регистрация: 20.11.2023
Сообщений: 129
29.12.2023, 08:30
По идее s - локальный объект, так что должен быть nullptr
0
Вездепух
Эксперт CЭксперт С++
 Аватар для TheCalligrapher
12930 / 6798 / 1820
Регистрация: 18.10.2014
Сообщений: 17,205
29.12.2023, 10:11  [ТС]
Цитата Сообщение от pechka_ne_sed Посмотреть сообщение
По идее s - локальный объект, так что должен быть nullptr
Нет. Никогда в С++ параметры не были полностью локальными объектами. Как я уже сказал выше, локальна у них только область видимости. Однако до С++17 их отличие от локальных объектов было весьма малозаметным и на него обычно не обращали внимания.

А вот С++17 внес изменения, которые могут очень сильно подчеркивать отличия параметров от локальных объектов. (В этом, собственно, и заключается ответ на данную головоломку.)
0
901 / 478 / 93
Регистрация: 10.06.2014
Сообщений: 2,700
29.12.2023, 12:31
Цитата Сообщение от TheCalligrapher Посмотреть сообщение
Ну почему же?
Выводы напрашиваются именно такие, потому что:

Когда мы возвращаем из функции foo указатель s.r, значение указателя присваивается в переменную p в main и казалось бы справедливо ожидать, что p будет ненулевым. Но хитрость в том, что после завершения функции будет вызван деструктор для объекта s, который присвоит в качестве значения для поля r nullptr. Но что такое r? Правильно, это ссылка на переменную p из main. В итоге получается:
1) сначала в main в переменную p будет присвоено значение s.r
2) после присваивания будет вызван деструктор, который сбросит значение переменной p в nullptr по ссылке

при этом я допускаю, что п.1 даже не будет выполнен, в силу его бессмысленности в данном случае (т.е возможно будет оптимизация)

Добавлено через 14 минут
Цитата Сообщение от TheCalligrapher Посмотреть сообщение
вполне может быть, что деструктор выполнит свое присваивание еще до того, как выполнится присваивание p = <результат вызова функции>
деструктор какого объекта? вроде как начиная с С++17, должен сработать copy elision (т.е аргумент будет сконструирован так, что он не будет копироваться из временного)

значит объект у нас будет всего один... и раз уж мы возвращаем поле этого единственного объекта, он этот объект не может быть уничтожен прежде чем значение его поля s.r будет записано в переменную p, а значит его деструктор будет вызван после присваивания.

в итоге получаем вышеописанный сценарий,
1) запишется адрес в p
2) адрес в p залунился по ссылке
0
19491 / 10097 / 2460
Регистрация: 30.01.2014
Сообщений: 17,805
29.12.2023, 17:44
TheCalligrapher, оно?
It is implementation-defined whether the lifetime of a parameter ends when the function in which it is defined returns or at the end of the enclosing full-expression.
The initialization and destruction of each parameter occurs within the context of the full-expression ([intro.execution]) where the function call appears.
http://eel.is/c++draft/expr.call#6
1
27 / 24 / 4
Регистрация: 20.11.2023
Сообщений: 129
29.12.2023, 18:47
Видимо, те самые правки из C++17 - CWG 1880.
Вот, цитирую:
Notes from the June, 2014 meeting:

WG decided to make it unspecified whether parameter objects are destroyed immediately following the call or at the end of the full-expression to which the call belongs. This approach also resolves issue 1935.
Поэтому это не уточненное поведение, оба правы .
Во общем порыскав по стандарту, это единственная более-менее не размытая формулировка. Может, от меня что-то и ускользнуло.
1
Вездепух
Эксперт CЭксперт С++
 Аватар для TheCalligrapher
12930 / 6798 / 1820
Регистрация: 18.10.2014
Сообщений: 17,205
29.12.2023, 21:04  [ТС]
Цитата Сообщение от Undisputed Посмотреть сообщение
деструктор какого объекта?
Деструктор объекта S s - параметра функции foo. Как я уже несколько раз указал в этой теме, объект s во все века и времена, начиная с С++98, создавался и уничтожался на уровень выше - в вызывающей функции main (!). Объект S s - параметр функции foo - на самом деле "принадлежит" функции main и незримо живет именно в функции main, а не в функции foo. И вопрос здесь лишь в том, когда именно этот объект будет уничтожен в функции main.

Почему-то этот уголок спецификации языка С++ упорно остается недопонятым и многие считают параметры функции чисто локальными объектами функции, которые якобы создаются и уничтожаются внутри функции. Это не так.

Правильную часть текста стандарта (и изменение в С++17) уже процитировали DrOffset и pechka_ne_sed. Да, правильный ответ: оба компилятора правы. Объект S s - параметр функции foo - может быть уничтожен в функции main сразу после возвращения из функции foo (как того требовал стандарт языка вплоть до С++17), а может быть уничтожен намного позже - в конце полного выражения, содержащего вызов функции foo (впервые разрешено в С++17).

Это изменение в стандарт было внесено под давлением GCC/Clang (читай: Itanium ABI), которые с начала времен по ряду причин упорно нарушали требования С++98-С++14, продлевая время жизни параметров функций до конца полного выражения. В конечном итоге такое поведение разрешили официально, как implementation-defined выбор из двух вариантов.

MSVC++ всегда придерживался "классической" спецификации - все объекты-параметры уничтожаются сразу же после завершения соответствующего вызова функции.

Цитата Сообщение от Undisputed Посмотреть сообщение
вроде как начиная с С++17, должен сработать copy elision (т.е аргумент будет сконструирован так, что он не будет копироваться из временного)
Тут нет и никогда не было никакого "временного". Во все века и времена параметр S s создавался непосредственно из аргумента p. Все объекты здесь - именованные. Ни одного временного тут нет и никогда не было. Поэтому и copy elision здесь совершенно ни при чем.

---

Наглядной и интересной иллюстрацией различия в поведении компиляторов (по первой и по второй модели удаления параметров) являются примеры вот такого рода

C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <iostream>
 
struct S
{
  static inline unsigned s_id;
  unsigned id;
  S() : id(++s_id) { std::cout << "S() " << id << std::endl; }
  S(const S &rhs) : id(++s_id) { std::cout << "S(const S &) " << id << " <- " << rhs.id << std::endl; }
  S &operator =(const S& rhs) { std::cout << "= " << id << " <- " << rhs.id << std::endl; return *this; }
  ~S() { std::cout << "~S() " << id << std::endl; }
};
 
int foo(S a, S b)
{
  std::cout << "foo " << a.id << " " << b.id << std::endl;
  return a.id + b.id;
}
 
int main() 
{
  int i;
  S c, d;
  std::cout << "Expression begin" << std::endl;
  i = foo(c, c) + foo(d, c) * foo(d, d), std::puts("Hello World"), foo(c, d), std::puts("Hello Again");
  std::cout << "Expression end" << std::endl;
}
Тут, как видите, в строке 24 присутствует длинное выражение, содержащее четыре вызова функции foo с параметрами a и b, за жизнью которых можно следить через их уникальные числовые идентификаторы S::id. (Если вы не доверяете этим идентификаторам, то их можно заменить на значение указателя this, но так будет сложнее сравнивать поведение разных запусков и разных компиляторов.)
  • В компиляторе MSVC++ каждый вызов foo создает пару параметров a и b и сразу же их уничтожает по возвращении из foo. Таким образом, в каждый момент времени существует не более одной пары a и b. Это - классическое повдение, требуемое спецификациями языка вплоть до C++14 включительно.
  • В компиляторах GCC и Clang для каждого вызова foo создается пара параметров a и b, которая однако продолжает жить и после возврата из вызова foo. Таким образом к концу вычисления полного выражения, после четырех вызовов foo, накапливается четыре пары одновременно живущих a и b. И только в самом конце, после "Hello Again", эти восемь объектов будет совместно уничтожены, один за другим.

Именно второй вариант поведения очень хорошо иллюстрирует тот факт, что "локальные параметры" функции на самом деле совсем не локальны - они живут в вызывающем коде. Локальна лишь область видимости идентификаторов таких объектов.

Для GCC:

Code
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
S() 1
S() 2
Expression begin
S(const S &) 3 <- 1
S(const S &) 4 <- 1
foo 4 3
S(const S &) 5 <- 1
S(const S &) 6 <- 2
foo 6 5
S(const S &) 7 <- 2
S(const S &) 8 <- 2
foo 8 7
Hello World
S(const S &) 9 <- 2
S(const S &) 10 <- 1
foo 10 9
Hello Again
~S() 10
~S() 9
~S() 8
~S() 7
~S() 6
~S() 5
~S() 4
~S() 3
Expression end
~S() 2
~S() 1
5
901 / 478 / 93
Регистрация: 10.06.2014
Сообщений: 2,700
29.12.2023, 21:05
Цитата Сообщение от TheCalligrapher Посмотреть сообщение
Тут нет и никогда не было никакого "временного"
На gcc, для стандартов ниже с++17 и с флагом -fno-elide-constructors создается два объекта. Откуда тогда берется создание второго объекта?
0
Вездепух
Эксперт CЭксперт С++
 Аватар для TheCalligrapher
12930 / 6798 / 1820
Регистрация: 18.10.2014
Сообщений: 17,205
29.12.2023, 21:22  [ТС]
Цитата Сообщение от Undisputed Посмотреть сообщение
а gcc, для стандартов ниже с++17 и с флагом -fno-elide-constructors создается два объекта. Откуда тогда берется создание второго объекта?
Логичное замечание.

Да, инициализация параметров при вызове функции делается по модели copy-initialization, то есть параметр S s инициализируется аргументом p так, как будто делалось объявление S s = p;, а "классическая" каноническая семантика такой инициализации сводилась к S s = S(p);. То есть тут действительно мог фигурировать еще и дополнительный временный объект (до эпохи guaranteed copy elision).

Однако исходная головоломка относится к современному С++ (С++17 и выше), в котором никакого временного объекта здесь быть не может.
0
901 / 478 / 93
Регистрация: 10.06.2014
Сообщений: 2,700
29.12.2023, 21:36
Цитата Сообщение от TheCalligrapher Посмотреть сообщение
Именно второй вариант поведения очень хорошо иллюстрирует тот факт, что "локальные параметры" функции на самом деле совсем не локальны - они живут в вызывающем коде.
Если принять это за ответ, то получается, что в общем мы не имеем права возвращать значение полей которые относятся к объекту-аргументу и полагать, что получим то, что хотели. Особенно грабли будут, если этот объект из какой нибудь библиотеки.

На мой взгляд это некорректно (наиболее правильный с моей точки зрения вариант развития событий я изложил в #14), и если эта неоднозначность действительно легальна, то её следует выключать по умолчанию и вывести в управление этой "фичей" через флаги компиляции...
0
Надоела реклама? Зарегистрируйтесь и она исчезнет полностью.
BasicMan
Эксперт
29316 / 5623 / 2384
Регистрация: 17.02.2009
Сообщений: 30,364
Блог
29.12.2023, 21:36
Помогаю со студенческими работами здесь

Ищу консультанта/учителя С под Linux, Для тех,кому интересно делиться знаниями
Здравствуйте! Начал изучать Си под Linux при решении реальных задач. Ищу человека, для изучения на постоянной основе. Если вы...

Написал программу от делать нечего, она оказалась полезной. А дальше что с ней делать?
Просто для себя накидал на C# за пару дней довольно узкоспециализированную и простенькую программу. Все кто видел ее, отмечают ее удобство...

Для тех, кому скучно
Тут есть простенькое задание на языке лисп уровня первого курса прикладной информатики. Кто хочет, может по приколу сделать и похвастаться...

Для тех, кому непонятна рекурсия
Всем привет! Два дня долбился долбился и никак не мог понять, как же работает рекурсия, заходил в гугл, смотрел и выходил, потому что...

Тест-игра (для тех, кому за 18)
Привет всем! Попался как то мне старый номер журнала PLAYBOY (купил как то в детстве ) и нашел там очень интересный тест-игру... (детям...


Искать еще темы с ответами

Или воспользуйтесь поиском по форуму:
20
Ответ Создать тему
Новые блоги и статьи
Модель микоризы: классовый агентный подход 3
anaschu 06.01.2026
aa0a7f55b50dd51c5ec569d2d10c54f6/ O1rJuneU_ls https:/ / vkvideo. ru/ video-115721503_456239114
Owen Logic: О недопустимости использования связки «аналоговый ПИД» + RegKZR
ФедосеевПавел 06.01.2026
Owen Logic: О недопустимости использования связки «аналоговый ПИД» + RegKZR ВВЕДЕНИЕ Введу сокращения: аналоговый ПИД — ПИД регулятор с управляющим выходом в виде числа в диапазоне от 0% до. . .
Модель микоризы: классовый агентный подход 2
anaschu 06.01.2026
репозиторий https:/ / github. com/ shumilovas/ fungi ветка по-частям. коммит Create переделка под биомассу. txt вход sc, но sm считается внутри мицелия. кстати, обьем тоже должен там считаться. . . .
Расчёт токов в цепи постоянного тока
igorrr37 05.01.2026
/ * Дана цепь постоянного тока с сопротивлениями и напряжениями. Надо найти токи в ветвях. Программа составляет систему уравнений по 1 и 2 законам Кирхгофа и решает её. Последовательность действий:. . .
Новый CodeBlocs. Версия 25.03
palva 04.01.2026
Оказывается, недавно вышла новая версия CodeBlocks за номером 25. 03. Когда-то давно я возился с только что вышедшей тогда версией 20. 03. С тех пор я давно снёс всё с компьютера и забыл. Теперь. . .
Модель микоризы: классовый агентный подход
anaschu 02.01.2026
Раньше это было два гриба и бактерия. Теперь три гриба, растение. И на уровне агентов добавится между грибами или бактериями взаимодействий. До того я пробовал подход через многомерные массивы,. . .
Советы по крайней бережливости. Внимание, это ОЧЕНЬ длинный пост.
Programma_Boinc 28.12.2025
Советы по крайней бережливости. Внимание, это ОЧЕНЬ длинный пост. Налог на собак: https:/ / **********/ gallery/ V06K53e Финансовый отчет в Excel: https:/ / **********/ gallery/ bKBkQFf Пост отсюда. . .
Кто-нибудь знает, где можно бесплатно получить настольный компьютер или ноутбук? США.
Programma_Boinc 26.12.2025
Нашел на реддите интересную статью под названием Anyone know where to get a free Desktop or Laptop? Ниже её машинный перевод. После долгих разбирательств я наконец-то вернула себе. . .
КиберФорум - форум программистов, компьютерный форум, программирование
Powered by vBulletin
Copyright ©2000 - 2026, CyberForum.ru