Здравствуйте, rg45, Вы писали:
R>Если добавить спецификацю доступа public в с2 ошибка уйдёт, но, опять же, от обоих вызовов — виртуального и невиртуального получим одинаковый эффект.
Тут моя ошибка, нужно было f() описывать в public-части. Давайте считать, что f() доступен для вызова.
Поинт в том, что с точки зрения C++ в конструкторе с4 мы автоматически получаем вызов самой свежей версии f на данный момент.
Это происходит за счет того, что С++ корректно модифицирует таблицу виртуальных методов по мере конструирования класса.
А значит, с формальной точки зрения, виртуальная диспетчеризация работает.
Повторюсь: суть в том, что ожидания неопытного пользователя о поведении виртуальной диспетчеризации в конструкторах/деструкторах, принципиально не совпадают с суровой реальностью стандарта С++
Здравствуйте, Лазар Бешкенадзе, Вы писали:
R>>Так а стоп. А почему тогда мы уверены, что она работает?
ЛБ>Потому что работает так как описано в стандарте.
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) Результат такого вызова будт точно таким же, как если бы мы позвали эту фунцию невиртуальным способом. Никаких утверждений о виртуальной диспетчеризации здесь нет.
--
Справедливость выше закона. А человечность выше справедливости.
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>А значит, с формальной точки зрения, виртуальная диспетчеризация работает.
Ну, это никак не проверяется через поведение программы. Результаты же идентичны.
--
Справедливость выше закона. А человечность выше справедливости.
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>Никаких утверждений о виртуальной диспетчеризации здесь нет.
Я и не писал что здесь есть такие утверждения.
Ты спросил как проверить — я тебе вежливо ответил. Ты юлишь. Я и с тобой прощаюсь.
Здравствуйте, rg45, Вы писали:
S>>А значит, с формальной точки зрения, виртуальная диспетчеризация работает.
R>Ну, это никак не проверяется через поведение программы. Результаты же идентичны.
Почему не проверяется? Согласно стандарту при вызове f() в конструкторе c4 мы получаем вызов наиболее свежей к этому времени версии f -- c2::f.
Значит все работает.
Аналогичная точка зрения у моих "оппонентов" и на ситуацию:
В деструкторе c1 будет вызвана наиболее свежая к этому времени версия f -- c1::f, а не c3::f.
В точности согласно стандарта, а значит все работает как и предписано. А значит все работает.
Т.е. работа "по стандарту" вполне себе проверяется через работу программы.
Вынужден повториться, но проблема здесь не в стандарте или возможности проверить работу согласно стандарта. А в том, что у части пользователей C++ другие ожидания от вызова виртуального метода в конструкторе/деструкторе.
S>Почему не проверяется? Согласно стандарту при вызове f() в конструкторе c4 мы получаем вызов наиболее свежей к этому времени версии f -- c2::f. S>Значит все работает.
Ну так, если мы сделаем невиртуальный вызов: с4::f() мы получим тот же самый вызов c2::f.
--
Справедливость выше закона. А человечность выше справедливости.
Здравствуйте, rg45, Вы писали:
S>>Почему не проверяется? Согласно стандарту при вызове f() в конструкторе c4 мы получаем вызов наиболее свежей к этому времени версии f -- c2::f. S>>Значит все работает.
R>Ну так, если мы сделаем невиртуальный вызов: с4::f() мы получим тот же самый вызов c2::f.
Так ведь это здесь не при чем, вот в чем фокус.
Есть контекст -- конструктор c4.
В нем вызывается виртуальный метод f.
Стандарт описывает какая именно версия f будет вызвана в этом контексте.
Мы этот вызов и получаем. ЧТД.
А то, что в этом контексте вызов this->f() не будет отличаться от вызова c2::f(), ну так это стечение обстоятельств.
Здравствуйте, so5team, Вы писали:
S>Есть контекст -- конструктор c4. S>В нем вызывается виртуальный метод f. S>Стандарт описывает какая именно версия f будет вызвана в этом контексте. S>Мы этот вызов и получаем. ЧТД.
S>А то, что в этом контексте вызов this->f() не будет отличаться от вызова c2::f(), ну так это стечение обстоятельств.
Давай пока забудем про вызов c2::f(). Мы рассматриваем конструктор класса f4 и для этого класса у нас есть два способа вызвать функцию f: 1) виртуальный — f() или this->f() и 2) невиртуальный — f4::f(). И согласно требованиям стандарта оба эти вызова должны дать один и тот же результат. Таким образом, на уровне поведения программы эти два способа неотличимы.
На всякий случай: я не утверждаю, что виртуальный вызов не выполняется. С точки зрения поведения программы это так же недоказуемо, как и неопровергаемо.
--
Справедливость выше закона. А человечность выше справедливости.
Здравствуйте, rg45, Вы писали:
R>Давай пока забудем про вызов c2::f(). Мы рассматриваем конструктор класса c4 и для этого класса у нас есть два способа вызвать функцию f: 1) виртуальный — f() или this->f() и 2) невиртуальный — c4::f(). И согласно требованиям стандарта оба эти вызова должны дать один и тот же результат. Таким образом, на уровне поведения программы эти два способа неотличимы.
Здравствуйте, so5team, Вы писали:
S>Здравствуйте, rg45, Вы писали:
R>>Давай пока забудем про вызов c2::f(). Мы рассматриваем конструктор класса c4 и для этого класса у нас есть два способа вызвать функцию f: 1) виртуальный — f() или this->f() и 2) невиртуальный — c4::f(). И согласно требованиям стандарта оба эти вызова должны дать один и тот же результат. Таким образом, на уровне поведения программы эти два способа неотличимы.
S>Неотличимы. И что из этого должно следовать?
Причем, неотличимы с точки зрения требований стандарта. Отсюда следует, что у нас нет никаких оснований, чтобы утверждать или отрицать "виртуальную диспетчеризацию" в конструкторах и деструкторах. О которой, кстати сказать, в стандарте вообще нет никаких упоминаний. Это термин, расчитанный на интуитивное восприятие. Тут было бы неплохо определиться для начала, о чём мы вообще спорим.
--
Справедливость выше закона. А человечность выше справедливости.
Здравствуйте, rg45, Вы писали:
R>Причем, неотличимы с точки зрения требований стандарта. Отсюда следует, что у нас нет никаких оснований, чтобы утверждать или отрицать "виртуальную диспетчеризацию" в конструкторах и деструкторах. О которой, кстати сказать, в стандарте вообще нет никаких упоминаний. Это термин, расчитанный на интуитивное восприятие. Тут было бы неплохо определиться для начала, о чём мы вообще спорим.
Так я же не зря озвучил то определение, которым пользуюсь в рассуждениях: "виртуальность в том и состоит, что мы дергаем метод через указатель/ссылку на базовый класс, а получаем вызов кода из производного класса". Уверен, что автор книги подразумевал тоже самое.
Именно это определение и нарушается, когда мы рассматриваем ситуацию с вызовом виртуальных методов в конструкторах/деструкторах. В С++.
В других "более лучших"(tm) языках оно-то и не нарушается.
Здравствуйте, so5team, Вы писали:
S>Так я же не зря озвучил то определение, которым пользуюсь в рассуждениях: "виртуальность в том и состоит, что мы дергаем метод через указатель/ссылку на базовый класс, а получаем вызов кода из производного класса". Уверен, что автор книги подразумевал тоже самое.
Так и как можно проверить, каким образом дёрнули метод? Разве что, заглянуть в сгенерированный код. Ну так, во-первых, это знание неправомерно будет обощать на все компиляторы/платформы/конфигурации. А во-вторых, оптимизатор имеет полное право заменить виртуальный вызов на невиртуальный в конструкторе/деструкторе. Он ведь знает, какой нужно дёрнуть метод прямо во время компиляции!
S>Именно это определение и нарушается, когда мы рассматриваем ситуацию с вызовом виртуальных методов в конструкторах/деструкторах. В С++.
Если не трудно, можно ещё раз указать пункт, который нарушается?
--
Справедливость выше закона. А человечность выше справедливости.
Здравствуйте, rg45, Вы писали:
S>>>То, что это к виртуальной диспетчеризации нормального человека не имеет отношения -- это уже другой вопрос. Но ведь работает же
M>>Это тоже виртуальная диспетчеризация
R>Ну, хорошо. Виртуальная диспетчеризация работает, но её результат неотличим от прямого невиртуального вызова. Нет возражений против такой формулировки?
R>Так а стоп. А почему тогда мы уверены, что она работает? В чём это проявляется и как это можно проверить?
Подожди. Если в текущем наследнике виртуальный метод не переопределён, то что ты собрался вызывать невиртуально?
Здравствуйте, rg45, Вы писали:
R>Причем, неотличимы с точки зрения требований стандарта. Отсюда следует, что у нас нет никаких оснований, чтобы утверждать или отрицать "виртуальную диспетчеризацию" в конструкторах и деструкторах. О которой, кстати сказать, в стандарте вообще нет никаких упоминаний. Это термин, расчитанный на интуитивное восприятие. Тут было бы неплохо определиться для начала, о чём мы вообще спорим.
По-моему, вы спорите о терминах
Я бы сказал, что суть виртуальных методов заключается в том, что производный класс может подменить некоторые методы базового класса, никак с этим базовым классом не договариваясь.
Такое вот динамические связывание.
А как это сделано, через указатель на функцию, или через указатель на таблицу функций, или просто компилятор умный и всегда правильно догадывается, это уже вообще детали реализации.
И поскольку это связывание динамическое, то вполне логично, что в процессе конструирования объекта с виртуальными методами связывание этих методов может меняться.
А дальше идёт простая оптимизация: пока отрабатывается конструктор базового класса, компилятор знает, что производный класс еще никто не создал, и может девиртуализировать виртуальный метод, позвав его напрямую. А может, собственно, этого и не делать, а позвать через указатель.
При всём при том, мне было бы удивительно, если бы еще на этапе конструирования базового класса виртуальные методы уже уходили бы в производный класс. Там к такому повороту судьбы могут быть еще не готовы, ведь тамошний конструктор еще не отработал.
Что конечно порождает, на взгляд новичка, некоторую путаницу. Но видимо, причина этой путаницы состоит не в неудачной реализации в плюсах, а в том, что сама по себе концепция виртуальных методов достаточно сложна и не допускает простую реализацию (хотя если реализовать ее по-сишному, в виде калбеков в структуре, которые "производная" структура может переключить на себя, всё это и не кажется какой-то непонятной магией).
Здравствуйте, rg45, Вы писали:
R>Так и как можно проверить, каким образом дёрнули метод?
А нам не важно как именно дернули -- через обращение к vtable или путем прямого вызова.
Нам важно какой метод в итоге был вызван. Т.е. конкретный механизм вызова всего лишь скучная деталь реализации.
S>>Именно это определение и нарушается, когда мы рассматриваем ситуацию с вызовом виртуальных методов в конструкторах/деструкторах. В С++.
R>Если не трудно, можно ещё раз указать пункт, который нарушается?
В точке (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) получим одинаковое поведение.
Здравствуйте, Marty, Вы писали:
M>Подожди. Если в текущем наследнике виртуальный метод не переопределён, то что ты собрался вызывать невиртуально?
Вот, смотри, как раз этот случай. Здесь c2::f — она же с3::f, она же с4::f. Т.е. функция, определённая в базовом классе c2::f, может быть вызвана в наследнике невиртуальным способом с использование имени класса наследника — с4::f. Так работает наследование.
#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{};
}
Ещё одним важным моментом является тот факт, что при вызове виртуальной функции в конструкторе или деструкторе, компилятор знает, какой нужно дёрнуть метод прямо во время компиляции! И на кой чёрт, спрашивается, вызывать этот метод через дополнительную косвенность, когда это можно сделать напрямую?
--
Справедливость выше закона. А человечность выше справедливости.
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, без вариантов. Да, он может сделать этот вызов через механизм позднего связывания. Только зачем? Где тот пункт стандарта, который обязывает его сделать только так?
--
Справедливость выше закона. А человечность выше справедливости.
Здравствуйте, 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 приводит к разному наблюдаемому поведению.