Re[14]: Книжка по UB
От: so5team https://stiffstream.com
Дата: 14.08.25 08:46
Оценка:
Здравствуйте, rg45, Вы писали:

R>Если добавить спецификацю доступа public в с2 ошибка уйдёт, но, опять же, от обоих вызовов — виртуального и невиртуального получим одинаковый эффект.


Тут моя ошибка, нужно было f() описывать в public-части. Давайте считать, что f() доступен для вызова.

Поинт в том, что с точки зрения C++ в конструкторе с4 мы автоматически получаем вызов самой свежей версии f на данный момент.
Это происходит за счет того, что С++ корректно модифицирует таблицу виртуальных методов по мере конструирования класса.

А значит, с формальной точки зрения, виртуальная диспетчеризация работает.

Повторюсь: суть в том, что ожидания неопытного пользователя о поведении виртуальной диспетчеризации в конструкторах/деструкторах, принципиально не совпадают с суровой реальностью стандарта С++
Re[11]: Книжка по UB
От: rg45 СССР  
Дата: 14.08.25 08:50
Оценка:
Здравствуйте, Лазар Бешкенадзе, Вы писали:

R>>Так а стоп. А почему тогда мы уверены, что она работает?


ЛБ>Потому что работает так как описано в стандарте.


Ну ты же сам приводил цитату из стандарта:

https://rsdn.org/forum/cpp/8977254.1
Автор: Лазар Бешкенадзе
Дата: 13.08 15:40


12.7 (3)
... When a virtual function is called directly or indirectly from a constructor (including from the mem-initializer for a data-member) or from a destructor, and the object to which a call applies is the object under construction or destruction, the function called is the one defined in the constructor or destructor's own class or in one of its bases, ...


Здесь написано 1) Виртуальную функцию можно вызвать из конструкторов и деструкторов (с чем никто и не спорил) и 2) Результат такого вызова будт точно таким же, как если бы мы позвали эту фунцию невиртуальным способом. Никаких утверждений о виртуальной диспетчеризации здесь нет.
--
Справедливость выше закона. А человечность выше справедливости.
Re[11]: Книжка по UB
От: so5team https://stiffstream.com
Дата: 14.08.25 08:52
Оценка:
Здравствуйте, Лазар Бешкенадзе, Вы писали:

ЛБ>Я не обязан проверять правильность работы иначе как сравнением с тем что написано в стандарте.


Когда кто-то так упорно ссылается на стандарт, то невольно вспоминается прекрасное: https://godbolt.org/g/o4HxtU
Не, ну а чо? Все по стандарту.
Re[15]: Книжка по UB
От: rg45 СССР  
Дата: 14.08.25 08:55
Оценка:
Здравствуйте, so5team, Вы писали:

S>Тут моя ошибка, нужно было f() описывать в public-части. Давайте считать, что f() доступен для вызова.


Хорошо, исправляем:

http://coliru.stacked-crooked.com/a/bcaa3ac0cf3c2746

class c1 {
public:
  virtual void f() { std::cout << "c1::f" << std::endl; }
};

class c2 : public c1 {
public:
  void f() override { std::cout << "c2::f" << std::endl;};
};

class c3 : public c2 {
  // нет своей версии f().
};

class c4 : public c3 {
public:
  c4() {
      f();      // c2::f
      c2::f();  // c2::f
      c3::f();  // c2::f
      c4::f();  // c2::f
    }
};

int main() {
    c4{};
}


S>Поинт в том, что с точки зрения C++ в конструкторе с4 мы автоматически получаем вызов самой свежей версии f на данный момент.

S>Это происходит за счет того, что С++ корректно модифицирует таблицу виртуальных методов по мере конструирования класса.

S>А значит, с формальной точки зрения, виртуальная диспетчеризация работает.


Ну, это никак не проверяется через поведение программы. Результаты же идентичны.
--
Справедливость выше закона. А человечность выше справедливости.
Отредактировано 14.08.2025 8:57 rg45 . Предыдущая версия .
Re[12]: Книжка по UB
От: Лазар Бешкенадзе СССР  
Дата: 14.08.25 09:02
Оценка:
Здравствуйте, rg45, Вы писали:

R>

R>12.7 (3)
R>... When a virtual function is called directly or indirectly from a constructor (including from the mem-initializer for a data-member) or from a destructor, and the object to which a call applies is the object under construction or destruction, the function called is the one defined in the constructor or destructor's own class or in one of its bases, ...


Ты уже валяешь дурака.

R>Здесь написано


R>1) Виртуальную функцию можно вызвать из конструкторов и деструкторов (с чем никто и не спорил)


Я неплохо владею английским и расскажу тебе по секрету — здесь это не написано.

R>2) Результат такого вызова будт точно таким же, как если бы мы позвали эту фунцию невиртуальным способом.


Здесь это не написано.

R>Никаких утверждений о виртуальной диспетчеризации здесь нет.


Я и не писал что здесь есть такие утверждения.

Ты спросил как проверить — я тебе вежливо ответил. Ты юлишь. Я и с тобой прощаюсь.

-
Re[16]: Книжка по UB
От: so5team https://stiffstream.com
Дата: 14.08.25 09:04
Оценка:
Здравствуйте, rg45, Вы писали:

S>>А значит, с формальной точки зрения, виртуальная диспетчеризация работает.


R>Ну, это никак не проверяется через поведение программы. Результаты же идентичны.


Почему не проверяется? Согласно стандарту при вызове f() в конструкторе c4 мы получаем вызов наиболее свежей к этому времени версии f -- c2::f.
Значит все работает.

Аналогичная точка зрения у моих "оппонентов" и на ситуацию:
struct c1 {
  ~c1() { f(); }
  virtual void f() { std::cout << "c1::f" << std::endl; }
};

struct c2 : public c1 {};

struct c3 : public c2 {
  void f() override { std::cout << "c3::f" << std::endl; }
};

int main() {
  c3 c;
}

В деструкторе c1 будет вызвана наиболее свежая к этому времени версия f -- c1::f, а не c3::f.
В точности согласно стандарта, а значит все работает как и предписано. А значит все работает.

Т.е. работа "по стандарту" вполне себе проверяется через работу программы.

Вынужден повториться, но проблема здесь не в стандарте или возможности проверить работу согласно стандарта. А в том, что у части пользователей C++ другие ожидания от вызова виртуального метода в конструкторе/деструкторе.
Re[13]: Книжка по UB
От: rg45 СССР  
Дата: 14.08.25 09:08
Оценка:
Здравствуйте, Лазар Бешкенадзе, Вы писали:

ЛБ>Ты уже валяешь дурака.


У-ру-ру. Ты не газуй только так сильно. А то я тебе сейчас расскжу, кто здесь кого валяет.
--
Справедливость выше закона. А человечность выше справедливости.
Отредактировано 14.08.2025 9:10 rg45 . Предыдущая версия .
Re[17]: Книжка по UB
От: rg45 СССР  
Дата: 14.08.25 09:13
Оценка:
Здравствуйте, so5team, Вы писали:


S>Почему не проверяется? Согласно стандарту при вызове f() в конструкторе c4 мы получаем вызов наиболее свежей к этому времени версии f -- c2::f.

S>Значит все работает.

Ну так, если мы сделаем невиртуальный вызов: с4::f() мы получим тот же самый вызов c2::f.
--
Справедливость выше закона. А человечность выше справедливости.
Re[18]: Книжка по UB
От: so5team https://stiffstream.com
Дата: 14.08.25 09:20
Оценка:
Здравствуйте, rg45, Вы писали:

S>>Почему не проверяется? Согласно стандарту при вызове f() в конструкторе c4 мы получаем вызов наиболее свежей к этому времени версии f -- c2::f.

S>>Значит все работает.

R>Ну так, если мы сделаем невиртуальный вызов: с4::f() мы получим тот же самый вызов c2::f.


Так ведь это здесь не при чем, вот в чем фокус.

Есть контекст -- конструктор c4.
В нем вызывается виртуальный метод f.
Стандарт описывает какая именно версия f будет вызвана в этом контексте.
Мы этот вызов и получаем. ЧТД.

А то, что в этом контексте вызов this->f() не будет отличаться от вызова c2::f(), ну так это стечение обстоятельств.
Re[19]: Книжка по UB
От: rg45 СССР  
Дата: 14.08.25 09:32
Оценка:
Здравствуйте, so5team, Вы писали:

S>Есть контекст -- конструктор c4.

S>В нем вызывается виртуальный метод f.
S>Стандарт описывает какая именно версия f будет вызвана в этом контексте.
S>Мы этот вызов и получаем. ЧТД.

S>А то, что в этом контексте вызов this->f() не будет отличаться от вызова c2::f(), ну так это стечение обстоятельств.


Давай пока забудем про вызов c2::f(). Мы рассматриваем конструктор класса f4 и для этого класса у нас есть два способа вызвать функцию f: 1) виртуальный — f() или this->f() и 2) невиртуальный — f4::f(). И согласно требованиям стандарта оба эти вызова должны дать один и тот же результат. Таким образом, на уровне поведения программы эти два способа неотличимы.

На всякий случай: я не утверждаю, что виртуальный вызов не выполняется. С точки зрения поведения программы это так же недоказуемо, как и неопровергаемо.
--
Справедливость выше закона. А человечность выше справедливости.
Отредактировано 14.08.2025 9:36 rg45 . Предыдущая версия .
Re[20]: Книжка по UB
От: so5team https://stiffstream.com
Дата: 14.08.25 09:37
Оценка:
Здравствуйте, rg45, Вы писали:

R>Давай пока забудем про вызов c2::f(). Мы рассматриваем конструктор класса c4 и для этого класса у нас есть два способа вызвать функцию f: 1) виртуальный — f() или this->f() и 2) невиртуальный — c4::f(). И согласно требованиям стандарта оба эти вызова должны дать один и тот же результат. Таким образом, на уровне поведения программы эти два способа неотличимы.


Неотличимы. И что из этого должно следовать?
Re[21]: Книжка по UB
От: rg45 СССР  
Дата: 14.08.25 09:43
Оценка:
Здравствуйте, so5team, Вы писали:

S>Здравствуйте, rg45, Вы писали:


R>>Давай пока забудем про вызов c2::f(). Мы рассматриваем конструктор класса c4 и для этого класса у нас есть два способа вызвать функцию f: 1) виртуальный — f() или this->f() и 2) невиртуальный — c4::f(). И согласно требованиям стандарта оба эти вызова должны дать один и тот же результат. Таким образом, на уровне поведения программы эти два способа неотличимы.


S>Неотличимы. И что из этого должно следовать?


Причем, неотличимы с точки зрения требований стандарта. Отсюда следует, что у нас нет никаких оснований, чтобы утверждать или отрицать "виртуальную диспетчеризацию" в конструкторах и деструкторах. О которой, кстати сказать, в стандарте вообще нет никаких упоминаний. Это термин, расчитанный на интуитивное восприятие. Тут было бы неплохо определиться для начала, о чём мы вообще спорим.
--
Справедливость выше закона. А человечность выше справедливости.
Re[22]: Книжка по UB
От: so5team https://stiffstream.com
Дата: 14.08.25 09:49
Оценка:
Здравствуйте, rg45, Вы писали:

R>Причем, неотличимы с точки зрения требований стандарта. Отсюда следует, что у нас нет никаких оснований, чтобы утверждать или отрицать "виртуальную диспетчеризацию" в конструкторах и деструкторах. О которой, кстати сказать, в стандарте вообще нет никаких упоминаний. Это термин, расчитанный на интуитивное восприятие. Тут было бы неплохо определиться для начала, о чём мы вообще спорим.


Так я же не зря озвучил то определение, которым пользуюсь в рассуждениях: "виртуальность в том и состоит, что мы дергаем метод через указатель/ссылку на базовый класс, а получаем вызов кода из производного класса". Уверен, что автор книги подразумевал тоже самое.

Именно это определение и нарушается, когда мы рассматриваем ситуацию с вызовом виртуальных методов в конструкторах/деструкторах. В С++.

В других "более лучших"(tm) языках оно-то и не нарушается.
Re[23]: Книжка по UB
От: rg45 СССР  
Дата: 14.08.25 09:57
Оценка:
Здравствуйте, so5team, Вы писали:

S>Так я же не зря озвучил то определение, которым пользуюсь в рассуждениях: "виртуальность в том и состоит, что мы дергаем метод через указатель/ссылку на базовый класс, а получаем вызов кода из производного класса". Уверен, что автор книги подразумевал тоже самое.


Так и как можно проверить, каким образом дёрнули метод? Разве что, заглянуть в сгенерированный код. Ну так, во-первых, это знание неправомерно будет обощать на все компиляторы/платформы/конфигурации. А во-вторых, оптимизатор имеет полное право заменить виртуальный вызов на невиртуальный в конструкторе/деструкторе. Он ведь знает, какой нужно дёрнуть метод прямо во время компиляции!

S>Именно это определение и нарушается, когда мы рассматриваем ситуацию с вызовом виртуальных методов в конструкторах/деструкторах. В С++.


Если не трудно, можно ещё раз указать пункт, который нарушается?
--
Справедливость выше закона. А человечность выше справедливости.
Re[10]: Книжка по UB
От: Marty Пират https://www.youtube.com/channel/UChp5PpQ6T4-93HbNF-8vSYg
Дата: 14.08.25 09:59
Оценка:
Здравствуйте, rg45, Вы писали:

S>>>То, что это к виртуальной диспетчеризации нормального человека не имеет отношения -- это уже другой вопрос. Но ведь работает же


M>>Это тоже виртуальная диспетчеризация


R>Ну, хорошо. Виртуальная диспетчеризация работает, но её результат неотличим от прямого невиртуального вызова. Нет возражений против такой формулировки?


R>Так а стоп. А почему тогда мы уверены, что она работает? В чём это проявляется и как это можно проверить?


Подожди. Если в текущем наследнике виртуальный метод не переопределён, то что ты собрался вызывать невиртуально?
Маньяк Робокряк колесит по городу
Re[22]: Книжка по UB
От: Pzz Россия https://github.com/alexpevzner
Дата: 14.08.25 10:05
Оценка:
Здравствуйте, rg45, Вы писали:

R>Причем, неотличимы с точки зрения требований стандарта. Отсюда следует, что у нас нет никаких оснований, чтобы утверждать или отрицать "виртуальную диспетчеризацию" в конструкторах и деструкторах. О которой, кстати сказать, в стандарте вообще нет никаких упоминаний. Это термин, расчитанный на интуитивное восприятие. Тут было бы неплохо определиться для начала, о чём мы вообще спорим.


По-моему, вы спорите о терминах

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

Такое вот динамические связывание.

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

И поскольку это связывание динамическое, то вполне логично, что в процессе конструирования объекта с виртуальными методами связывание этих методов может меняться.

А дальше идёт простая оптимизация: пока отрабатывается конструктор базового класса, компилятор знает, что производный класс еще никто не создал, и может девиртуализировать виртуальный метод, позвав его напрямую. А может, собственно, этого и не делать, а позвать через указатель.

При всём при том, мне было бы удивительно, если бы еще на этапе конструирования базового класса виртуальные методы уже уходили бы в производный класс. Там к такому повороту судьбы могут быть еще не готовы, ведь тамошний конструктор еще не отработал.

Что конечно порождает, на взгляд новичка, некоторую путаницу. Но видимо, причина этой путаницы состоит не в неудачной реализации в плюсах, а в том, что сама по себе концепция виртуальных методов достаточно сложна и не допускает простую реализацию (хотя если реализовать ее по-сишному, в виде калбеков в структуре, которые "производная" структура может переключить на себя, всё это и не кажется какой-то непонятной магией).
Re[24]: Книжка по UB
От: so5team https://stiffstream.com
Дата: 14.08.25 10:07
Оценка:
Здравствуйте, rg45, Вы писали:

R>Так и как можно проверить, каким образом дёрнули метод?


А нам не важно как именно дернули -- через обращение к vtable или путем прямого вызова.
Нам важно какой метод в итоге был вызван. Т.е. конкретный механизм вызова всего лишь скучная деталь реализации.

S>>Именно это определение и нарушается, когда мы рассматриваем ситуацию с вызовом виртуальных методов в конструкторах/деструкторах. В С++.


R>Если не трудно, можно ещё раз указать пункт, который нарушается?


struct s1 {
  virtual void f() { std::cout << "s1::f" << std::endl; }

  s1() {
    this->f(); // (1)
  }

  void call_f() {
    this->f(); // (3)
  }
};

struct s2 : public s1 {
  void f() override { std::cout << "s2::f" << std::endl; }
};

int main() {
  s2 s; // (2)

  s.call_f(); // (4)
}


В точке (2) у нас конструируется объект типа s2, у которого должен быть собственный переопределенный f.

В точке (1) у нас на руках указатель на базовый тип s1 (это this через который идет вызов f).
Но в точке (1) вызов виртуального метода через указатель на базовый тип мы получаем не вызов s2::f, а вызов s1::f.

В точке (4) у нас все тот же объект типа s2.
Через вызов call_f мы приходим в точку (3), в которой у нас на руках опять указатель на базовый тип s1.
Однако, в точке (3) вызов виртуального метода через указатель на базовый тип мы получаем вызов s2::f, а не s1::f.

Тогда как в Java, Ruby или Python (подозреваю, что и в C#, и в D) мы в точках (1) и (3) получим одинаковое поведение.
Re[11]: Книжка по UB
От: rg45 СССР  
Дата: 14.08.25 10:08
Оценка: +1
Здравствуйте, Marty, Вы писали:

M>Подожди. Если в текущем наследнике виртуальный метод не переопределён, то что ты собрался вызывать невиртуально?


Вот, смотри, как раз этот случай. Здесь c2::f — она же с3::f, она же с4::f. Т.е. функция, определённая в базовом классе c2::f, может быть вызвана в наследнике невиртуальным способом с использование имени класса наследника — с4::f. Так работает наследование.

http://coliru.stacked-crooked.com/a/bcaa3ac0cf3c2746

#include <iostream>

class c1 {
public:
  virtual void f() { std::cout << "c1::f" << std::endl; }
};

class c2 : public c1 {
public:
  void f() override { std::cout << "c2::f" << std::endl;};
};

class c3 : public c2 {
  // нет своей версии f().
};

class c4 : public c3 {
public:
  c4() {
      f();      // c2::f
      c2::f();  // c2::f
      c3::f();  // c2::f
      c4::f();  // c2::f
    }
};

int main() {
    c4{};
}


Ещё одним важным моментом является тот факт, что при вызове виртуальной функции в конструкторе или деструкторе, компилятор знает, какой нужно дёрнуть метод прямо во время компиляции! И на кой чёрт, спрашивается, вызывать этот метод через дополнительную косвенность, когда это можно сделать напрямую?
--
Справедливость выше закона. А человечность выше справедливости.
Re[25]: Книжка по UB
От: rg45 СССР  
Дата: 14.08.25 10:17
Оценка:
Здравствуйте, so5team, Вы писали:

S>
S>struct s1 {
S>  virtual void f() { std::cout << "s1::f" << std::endl; }

S>  s1() {
    this->>f(); // (1)
S>  }

S>  void call_f() {
    this->>f(); // (3)
S>  }
S>};

S>struct s2 : public s1 {
S>  void f() override { std::cout << "s2::f" << std::endl; }
S>};

S>int main() {
S>  s2 s; // (2)

S>  s.call_f(); // (4)
S>}
S>


S>В точке (2) у нас конструируется объект типа s2, у которого должен быть собственный переопределенный f.


S>В точке (1) у нас на руках указатель на базовый тип s1 (это this через который идет вызов f).

S>Но в точке (1) вызов виртуального метода через указатель на базовый тип мы получаем не вызов s2::f, а вызов s1::f.

S>В точке (4) у нас все тот же объект типа s2.

S>Через вызов call_f мы приходим в точку (3), в которой у нас на руках опять указатель на базовый тип s1.
S>Однако, в точке (3) вызов виртуального метода через указатель на базовый тип мы получаем вызов s2::f, а не s1::f.

В точке (3) компилятор не знает, какой будет метод вызван и вынужден использовать позднее связывание. А в точке (1) компилятор уже во время компиляции знает, что здесь может быть вызван только s1::f, без вариантов. Да, он может сделать этот вызов через механизм позднего связывания. Только зачем? Где тот пункт стандарта, который обязывает его сделать только так?
--
Справедливость выше закона. А человечность выше справедливости.
Отредактировано 14.08.2025 10:18 rg45 . Предыдущая версия .
Re[26]: Книжка по UB
От: so5team https://stiffstream.com
Дата: 14.08.25 10:25
Оценка:
Здравствуйте, rg45, Вы писали:

R>В точке (3) компилятор не знает, какой будет метод вызван и вынужден использовать позднее связывание. А в точке (1) компилятор уже во время компиляции знает, что здесь может быть вызван только s1::f, без вариантов. Да, он может сделать этот вызов через механизм позднего связывания. Только зачем?


Извините, но вот эта часть мне не интересна от слова совсем.
И, как мне кажется, обсуждаемая глава книги вовсе не про то, что знает компилятор во время компиляции.

PS. Для разнообразия чуть измените конструктор s1:
struct s1 {
  virtual void f() { std::cout << "s1::f" << std::endl; }

  s1() {
    call_f();
  }

  void call_f();
};

// Где-то совсем в другом месте, возможно даже в другой единице трансляции.
void s1::call_f() {
  this->f(); // (3)
}

У нас теперь нет точки (1) в которой компилятор что-то точно знает.
Зато есть точка (3), которая на разных путях к call_f приводит к разному наблюдаемому поведению.
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.