Подтвердить что ты не робот

Почему виртуальные функции не могут использовать вычет типа возврата?

n3797 говорит:

§ 7.1.6.4/14:

Функция, объявленная с типом возврата, который использует тип-заполнитель не должны быть виртуальными (10.3).

Поэтому следующая программа плохо сформирована:

struct s
{
    virtual auto foo()
    {
    }
};

Все, что я могу найти для обоснования, - это неопределенный однострочный шрифт из n3638:

виртуальный

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

Может ли кто-нибудь дать дополнительное обоснование или дать хороший (код) пример, который согласуется с приведенной выше цитатой?

4b9b3361

Ответ 1

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

Например, если вы видите оператор return, который выглядит как

return a * 3 + b;

вам нужно будет отследить программу обратно до точки объявления a и b, выяснить, как продвигаются типы, и решить, какой тип возврата должен быть.

Похоже, что разработчики языка поняли, что это будет довольно запутанным, и решил не разрешать эту функцию.

Ответ 2

Ну, выводимый возвращаемый тип функции становится известен только в точке определения функции: тип возврата выводится из операторов return внутри тела функции.

Между тем, vtable построена, и переопределенная семантика проверяется на основе только объявлений функций, присутствующих в определении класса. Эти проверки никогда не полагались на определение функции и никогда не нуждались в определении. Например, язык требует, чтобы функция переопределения имела один и тот же тип возврата или ковариантный тип возврата в качестве функции, которую он переопределяет. Если объявление неопределяющей функции задает тип возвращаемого возврата (т.е. auto без возвращаемого типа возврата), его тип возврата неизвестен в этой точке и остается неизвестным до тех пор, пока компилятор не встретит определение функции. Невозможно выполнить вышеупомянутую проверку типа возврата, когда тип возврата неизвестен. Просить компилятор как-то отложить проверку типа возврата до того момента, когда он станет известен, потребует серьезной качественной реорганизации этой фундаментальной области спецификации языка. (Я не уверен, что это возможно.)

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

В принципе, по той же причине вы не можете применить оператор & к функции, объявленной как auto f();, но еще не определенной, как показывает пример в 7.1.6.3/11.

Ответ 3

auto - неизвестный тип в уравнении типа; как обычно, тип должен быть определен в какой-то момент. Виртуальная функция должна иметь определение, она всегда "используется", даже если функция никогда не вызывается в программе.

Краткое описание проблемы vtable

Ковариантные типы возвращаемых данных являются проблемой реализации с помощью vtable: ковариантные возвращает - внутренне мощная функция (затем кастрированная произвольными языковыми правилами). Ковариация ограничена указателями (и ссылками), полученными для базовых преобразований, но внутренняя мощность и, следовательно, сложность реализации - это почти одно из произвольных преобразований: полученное от суммы основания к произвольному коду (полученному из базы, ограниченному субобъектами исключительного базового класса, aka не виртуальное наследование, было бы намного проще).

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

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

Подробное объяснение

Основная теория vtables и первичных баз

struct Primbase {
    virtual void foo(); // new
};

struct Der 
     : Primbase { // primary base 
    void foo(); // replace Primbase::foo()
    virtual void bar(); // new slot
};

Primbase является основной базой здесь, она начинается с того же адреса на производном объекте. Это чрезвычайно важно: для первичной базы конверсии вверх/вниз могут выполняться с помощью реинтерпрета или с помощью стиля C в сгенерированном коде. Единственное наследование намного проще для исполнителя, потому что есть только первичные базовые классы. При множественном наследовании необходима арифметика указателя.

В Der имеется только один vptr, один из Primbase; существует один vtable для Der, макет совместим с vtable Primbase.

Здесь обычный компилятор не выделяет еще один слот для Der::foo() в vtable, так как фактически вызывается производная функция (в гипотетическом сгенерированном C-коде) с указателем Primbase* this, а не Der*, Der vtable имеет только два слота (плюс данные RTTI).

Первичная ковариация

Теперь добавим некоторую простую ковариацию:

struct Primbase {
    virtual Primbase *foo(); // new slot in vtable
};

struct Der 
     : Primbase { // primary base 
    Der *foo(); // replaces Primbase::foo() in vtable
    virtual void bar(); // new slot
};

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

Коэффициент смещения нулевой нулевой точки

Более сложный:

struct Basebelow {
    virtual void bar(); // new slot
};

struct Primbase {
    virtual Basebelow *foo(); // new
};

struct Der 
     : Primbase, // primary base 
       Basebelow { // base at a non zero offset
    Der *foo(); // new slot?
};

Здесь представление Der* не совпадает с представлением указателя субобъекта его базового класса Basebelow*. Два варианта реализации:

  • (оставьте) соглашайтесь с интерфейсом виртуального вызова Basebelow *(Primbase::foo)() для всей иерархии: this является Primbase* (совместим с Der*), но возвращаемое значение типа несовместимо (другое представление), поэтому реализация производной функции преобразует Der* в Primbase* (арифметику указателя) и вызывающего абонента с обратным обращением при выполнении виртуального вызова на Der;

  • (ввести) еще один слот виртуальной функции в Der vtable для функции, возвращающей a Der*.

Обобщены в иерархии совместного использования: виртуальная ковариация

В общем случае подобъекты базового класса совместно используются другим производным классом, это виртуальный "алмаз":

struct B {};
struct L : virtual B {};
struct R : virtual B {};
struct D : L, R {};

Здесь преобразование в B* является динамическим, основанным на типе времени выполнения (часто с использованием vptr или внутренних указателей/смещений в объектах, как в MSVC).

В общем, такие преобразования в подобъект базового класса теряют информацию и не могут быть отменены. Нет надежного преобразования B* в L* вниз. Следовательно, выбор (оседать) недоступен. Реализация будет иметь (ввести).

Вернуться к auto issue

(ввести) не является сложным вариантом реализации, но он увеличивает vtable: макет vtable определяется числом (вводить).

Таким образом, расположение vtable определяется количеством виртуальных функций (которые мы знаем из определения класса), наличие ковариантных виртуальных функций (что мы можем знать только из типов возвращаемых функций) и тип ковариации: первичная ковариация, ненулевая ковариантность или виртуальная ковариация.

Заключение

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

Это усложнит реализацию.

Примечание: используются термины типа "виртуальная ковариация", кроме "первичной базы", ​​которая официально определена в Itanium С++ ABI.

EDIT: Почему я считаю, что проверка ограничений не является проблемой

Проверка ковариантных ограничений не является проблемой, не разбивает отдельную компиляцию или модель С++:

auto переопределитель функции возврата указателя класса (/ref)

struct B {
    virtual int f();
    virtual B *g();
};

struct D : B {
    auto f(); // int f() 
    auto g(); // ?
};

Тип f() полностью ограничен, и определение функции должно возвращать int.

Тип возврата g() частично ограничен: он может быть B* или некотором derived_from_B*. Проверка будет выполняться в точке определения.

Переопределение автоматической виртуальной функции

Рассмотрим потенциальный производный класс D2:

struct D2 : D {
    T1 f(); // T1 must be int 
    T2 g(); // ?
};

Здесь можно проверить ограничения на f(), так как T1 должен быть int, но не ограничения на T2, поскольку объявление D::g() неизвестно. Все, что мы знаем, это то, что T2 должен быть указателем на подкласс B (возможно, только B).

Определение D::g() может быть ковариантным и ввести более сильное ограничение:

auto D::g() { 
    return new D;
} // covariant D* return

поэтому T2 должен быть указателем на класс, полученный из D (возможно, только D).

Прежде чем просмотреть определение, мы не можем знать это ограничение.

Поскольку переопределение объявления не может быть проверено перед просмотром определения, оно должно быть отклонено.

Для простоты, я думаю, f() также должен быть отклонен.