Использование объявления в качестве переопределения

У нас есть следующий простой (и немного измененный, чтобы добавить main и вывод) пример в стандарте:

struct A {
    virtual void f()
    {
        cout << "A\n";
    }
};

struct B : virtual A {
    virtual void f()
    {
        cout << "B\n";
    }
};

struct C : B, virtual A {
    using A::f;
};

int main()
{
    C c;
    c.f();              // calls B​::​f, the final overrider
    c.C::f();
    return 0;
}

Из чего можно сделать вывод, что using A::f не представляет переопределения. Но какая формулировка Стандарта диктует это? Вот формулировка окончательного переопределения из черновика C++17 ([class.virtual]p2):

‹...> Виртуальная функция-член C::vf объекта класса S является окончательным переопределением, если только самый производный класс (4.5), из которого S является подобъектом базового класса (если таковой имеется), объявляет или наследует другой член функция, которая переопределяет vf. В производном классе, если виртуальная функция-член подобъекта базового класса имеет более одного финального переопределения, программа имеет неправильный формат.

И я не смог найти, что на самом деле означает «переопределение». Если он не определен и мы рассматриваем любое объявление как переопределение, тогда мы должны рассматривать объявление using как переопределение, поскольку [namespace.udecl]p2 говорит:

Каждое использование-объявление является объявлением и объявлением-членом и поэтому может использоваться в определении класса.

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


Рассмотрим следующий код:

#include <iostream>
#include <string>

using std::cout;

class A {
public:
    virtual void print() const {
        cout << "from A" << std::endl;
    }
};

class B: public A {
public:
    void print() const override {
        cout << "from B" << std::endl;
    }
};

class C: public A {
public:
    void print() const override {
        cout << "from C" << std::endl;
    }
};

class D: public B, public C {
public:
    using C::print;
};

int main()
{
    D d{};
    d.print();
    return 0;
}

Если объявление using не вводит переопределение, то у нас есть 2 окончательных переопределения в D, следовательно, неопределенное поведение из-за

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

Верно?


person ixSci    schedule 29.10.2018    source источник


Ответы (1)


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

[dcl.dcl]

1 Объявления обычно указывают, как следует интерпретировать имена. Объявления имеют вид

declaration:
  block-declaration
  nodeclspec-function-declaration
  function-definition
  template-declaration
  deduction-guide
  explicit-instantiation
  explicit-specialization
  linkage-specification
  namespace-definition
  empty-declaration
  attribute-declaration

block-declaration:
  simple-declaration
  asm-definition
  namespace-alias-definition
  using-declaration
  using-directive
  static_assert-declaration
  alias-declaration
  opaque-enum-declaration

nodeclspec-function-declaration:
  attribute-specifier-seq declarator ;

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

[namespace.udecl]

15 Когда using-declarator приносит объявления из базового класса в производный класс, функции-члены и шаблоны функций-членов в производном классе переопределяют и/или скрывают функции-члены и шаблоны функций-членов с тем же именем, списком типов параметров, квалификатором cv и квалификатором ссылки (если есть) в базовый класс (а не конфликтующий). Такие скрытые или переопределенные объявления исключаются из набора объявлений, введенных с помощью объявления-использования.

16 В целях устранения перегрузки функции, введенные объявлением использования в производный класс, обрабатываются так, как если бы они были членами производного класса. В частности, неявный параметр this должен рассматриваться как указатель на производный класс, а не на базовый класс. Это не влияет на тип функции, и во всех других отношениях функция остается членом базового класса.

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

[класс.виртуальный]

2 Если в классе объявлена ​​виртуальная функция-член vf Base и в классе Derived, производном прямо или косвенно от Base, объявляется функция-член vf с тем же именем, списком типов параметров, квалификацией cv и спецификатором ref (или их отсутствием), что и Base​::​vf, затем Derived​::​vf также является виртуальным (независимо от того, объявлено оно так или нет) и переопределяет Base​::​vf. Для удобства мы говорим, что любая виртуальная функция переопределяет саму себя.

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

Текущая формулировка частично взята из дефекта 608 CWG. Он призван прояснить проблемную интерпретацию в этом отчете и отделить использование объявлений от понятия переопределения виртуальных функций.


Что касается вашего второго вопроса, важно отметить, что в этой цитате важно отметить «базового класса подобъект». В вашем примере кода есть два A подобъекта в D (наследование в этом примере не является виртуальным). И у каждого есть свой окончательный переопределение в B и C соответственно. Таким образом, программа не является неправильной, с объявлением другого переопределения или без него в D.

Параграф, который вас интересует, относится к случаю виртуального наследования. Если бы B и C имели виртуальную базу A, а D унаследовано от обоих без переопределения print, программа быть неправильно сформированным. И использование объявления, такого как using C::print, не сделает его правильно сформированным, опять же по причинам, указанным выше.

person StoryTeller - Unslander Monica    schedule 29.10.2018
comment
Другими словами, оператор using объявляет имя, но не отдельную функцию, поэтому, хотя мы можем получить доступ к функции через ее имя в C, нет ничего, что могло бы стать переопределением, поскольку отдельной функции в C не существует. - person Lightness Races in Orbit; 29.10.2018
comment
Хм, хорошо, я вижу вашу логику, но если это не УБ, то что он должен вызывать и почему? Я имею в виду, что d.print() - это виртуальный вызов, и он должен решать, какую функцию вызывать, и у нас есть 2. Что делать? - person ixSci; 29.10.2018
comment
@ixSci - d.print() должен сначала выполнить поиск, чтобы определить, какую функцию вызывать. Поскольку в вашем коде есть объявление using C::print;, это print будет найдено однозначно. Теперь, поскольку d не является указателем или ссылкой, он будет отправлен в обычном режиме. Объявление using позволяет вам определить, как будет работать поиск имени, но не вводит объявления функций. Имеет ли это смысл? - person StoryTeller - Unslander Monica; 29.10.2018
comment
@StoryTeller, отчасти это так. Но вы можете изменить тип на ссылку или указатель, и тот же вопрос остается актуальным. Более того, если вы посмотрите на первый пример в моем вопросе (отрывок из стандарта), вы увидите тот же шаблон, но он вызывает B::f, а не A::f, выполняя динамическую отправку, не используя информацию, полученную при использовании объявления. - person ixSci; 29.10.2018
comment
@ixSci - Если d было D&, то потребуется вызов виртуальной функции, чтобы вызвать переопределение C::f. Тип d повлияет на вызов, но идентификатор вызываемой функции (возможно, виртуально) частично определяется объявлением using. Каждый вызов виртуальной функции состоит из двух частей. Поиск имени + разрешение перегрузки, чтобы найти имя вызываемой функции (на которое влияет использование объявлений), за которым следует вызов виртуальной функции (на который влияет переопределение, но не использование объявлений). Я бы сказал, что пример в вашем вопросе соответствует этому. - person StoryTeller - Unslander Monica; 29.10.2018
comment
Ну, насколько я понимаю, это противоречит тому факту, что использование объявления не обеспечивает окончательных переопределений. Потому что, если это не так, он не может участвовать в разрешении виртуальной отправки. В более старых стандартах (по крайней мере, в 03) есть цитата, в которой говорится следующее: Правила поиска членов (10.2) используются для определения окончательного переопределения виртуальной функции в области действия производного класса, но игнорируются имена, введенные с помощью деклараций использования. Я не понимаю, почему они удалили это, но оно все еще было там. - person ixSci; 29.10.2018
comment
@ixSci - формулировка отсутствует, потому что она была предметом дефекта (608). Не стоит цепляться за эти слова. Новая формулировка введена именно для того, чтобы отличить переопределения от идентичности того, что они переопределяют (и идентичности, которая может быть определена с помощью объявления использования). - person StoryTeller - Unslander Monica; 29.10.2018
comment
Спасибо за объяснение и этот отчет о дефекте! Пожалуйста, добавьте его (я имею в виду ссылку на DR) в ответ, так как это помогает дополнить картину. Я подумаю об этом некоторое время и приму ваш ответ. - person ixSci; 29.10.2018
comment
@ixSci - Конечно. Добавил пару слов об отчете. - person StoryTeller - Unslander Monica; 29.10.2018
comment
В соответствии с этим, нет ли менее подробного способа сказать, что виртуальная функция должна использовать конкретную реализацию базового класса? - person Treviño; 16.10.2020
comment
@Treviño - Не то, чтобы я знал об этом - person StoryTeller - Unslander Monica; 16.10.2020