Изменение сигнатуры виртуальной функции
От: Muxa  
Дата: 18.04.23 14:13
Оценка:
Есть два класса — первый определяет интерфейс, второй его реализует:
class I
{
    virtual I* F0() = 0;
    virtual I& F1() = 0;
    virtual void F2(I*) = 0;
    virtual void F3(I&) = 0;
};

class C : public I
{
    C* F0() override { return this; }
    C& F1() override { return *this; }
    void F2(C*) override {}
    void F3(C&) override {}
};
https://godbolt.org/z/qd4so7TW3

В сигнатурах функций-реализаций я заменил тип I на C.

Первые две функции компилятор (gcc/clang) прожевывает без проблем, а на вторые две выдает ошибку
'void C::F2(C*)' marked 'override', but does not override


Почему компилятор такое позволяет для F0 и F1, но не позволяет для F2 и F3?
Отредактировано 18.04.2023 14:15 Muxa . Предыдущая версия .
Re: Изменение сигнатуры виртуальной функции
От: Coreman  
Дата: 18.04.23 14:18
Оценка: 4 (1) -1
Здравствуйте, Muxa, Вы писали:

...
M>В сигнатурах функций-реализаций я заменил тип I на C.
...

Потому что в данном конкретном случае, тип возвразаемого значения не является частью сигнатуры функции.
Re: Изменение сигнатуры виртуальной функции
От: B0FEE664  
Дата: 18.04.23 14:20
Оценка: 4 (1) +1
Здравствуйте, Muxa, Вы писали:

M>Почему компилятор такое позволяет для F0 и F1, но не позволяет для F2 и F3?

здесь
И каждый день — без права на ошибку...
Re: Изменение сигнатуры виртуальной функции
От: Zhendos  
Дата: 18.04.23 14:39
Оценка:
Здравствуйте, Muxa, Вы писали:

M>Почему компилятор такое позволяет для F0 и F1, но не позволяет для F2 и F3?


По-моему вполне логично. Функция должна отдавать "I *" и она продолжает отдавать
"I *" ("C *" частный случай "I *").

А вот замена аргумента с "I *" на "C *" не работает,
так как что должен делать компилятор когда на вход будет подаваться
"I *", а не "C *"?
Re: Изменение сигнатуры виртуальной функции
От: rg45 СССР  
Дата: 18.04.23 15:02
Оценка: 4 (1) +3
Здравствуйте, Muxa, Вы писали:

M>Почему компилятор такое позволяет для F0 и F1, но не позволяет для F2 и F3?


Относительно F0 и F1 в стандарте языка есть ясное правило, разрешающее ковариантность возвращаемых типов для виртуальных функций:

https://timsong-cpp.github.io/cppwp/class.virtual#8

The return type of an overriding function shall be either identical to the return type of the overridden function or covariant with the classes of the functions.
If a function D​::​f overrides a function B​::​f, the return types of the functions are covariant if they satisfy the following criteria:
    - both are pointers to classes, both are lvalue references to classes, or both are rvalue references to classes102
    - the class in the return type of B​::​f is the same class as the class in the return type of D​::​f, or is an unambiguous and accessible direct or indirect base class of the class in the return type of D​::​f
    - both pointers or references have the same cv-qualification and the class type in the return type of D​::​f has the same cv-qualification as or less cv-qualification than the class type in the return type of B​::​f.


Почему сделано так а не иначе, объяснения, данные здесь
Автор: Coreman
Дата: 18.04.23
и здесь
Автор: Zhendos
Дата: 18.04.23
, на мой взгляд, выглядят вполне разумными.
--
Не можешь достичь желаемого — пожелай достигнутого.
Отредактировано 18.04.2023 15:04 rg45 . Предыдущая версия . Еще …
Отредактировано 18.04.2023 15:03 rg45 . Предыдущая версия .
Re[2]: To watchmaker
От: rg45 СССР  
Дата: 18.04.23 18:47
Оценка:
Здравствуйте, Coreman, Вы писали:

C>Потому что в данном конкретном случае, тип возвразаемого значения не является частью сигнатуры функции.


Озадачен минусом от watchmaker. Вот, что говорит стандарт:

https://timsong-cpp.github.io/cppwp/intro.defs#defns.signature.member

3.56 signature
⟨class member function⟩ name, parameter-type-list, class of which the function is a member, cv-qualifiers (if any), ref-qualifier (if any), and trailing requires-clause (if any)


watchmaker, не прокомментируешь, с чем не согласен?
--
Не можешь достичь желаемого — пожелай достигнутого.
Re: Изменение сигнатуры виртуальной функции
От: Кодт Россия  
Дата: 18.04.23 21:35
Оценка: 17 (2) +2
Здравствуйте, Muxa, Вы писали:

M>Почему компилятор такое позволяет для F0 и F1, но не позволяет для F2 и F3?


Ковариантность по возвращаемому типу — логична и не противоречит принципу подстановки.
Type Base::fun();
SubType Derived::fun();

Type t = ((Base&)derived).fun();  // да, t фактически SubType


Также принципу подстановки не противоречит и контравариантность по входящему типу
void Base::fun(SubType);
void Derived::fun(Type t);

SubType s;
((Base&)derived).fun(s);  // t фактически SubType


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

Почему для указателей и ссылок, — понятно. Потому что у них всех одинаковое бинарное представление и элементарный static_cast, что позволяет вызывать виртуальные функции и сразу знать, что делать с результатом.
(Хотя, если SubType — это указатель на структуру со смещением базы, или с виртуальным наследованием, там всё равно компилятору приходится делать функцию-прокладку, которая этот static_cast выполнит).

Почему только для возвращаемого типа, — чтобы себе не забивать голову.
Например, у нас есть такая весёлая штука, как перегрузка.
void Base::fun(S);

void Derived::fun(T1);
void Derived::fun(T2);

где T1 и T2 — супертипы S. Неважно, равноправные или один тоже субтип другого.
Вопрос: которая из этих функций переопределяет функцию базы?

А если у базы тоже перегруженные функции, какая функция наследника переопределит какую функцию базы?

Вот поэтому, — из-за того, что выбор перегрузки определяется типами аргументов, — и принято волюнтаристское решение, что по аргументам функции инвариантны, а не контравариантны.

Ах да, кстати. Контравариантность в С++ появляется в работе с указателями на члены. И всё.
void Base::foo();
void Derived::bar();
// Derived::foo тоже есть, - унаследовано или переопределено, неважно

void(Base::*base_member)() = &Base::foo;
void(Derived::*derived_member)() = base_member;  // ok

(derived.*base_member)();  // ok, у наследника валидные члены базы
(derived.*derived_member)();  // ok
(base.*base_member)();  // ok
(base.*derived_member)();  // не ок, потому что а вдруг там Derived::bar, который не умеет работать с this типа Base* ?
Перекуём баги на фичи!
Re[2]: Изменение сигнатуры виртуальной функции
От: Voivoid Россия  
Дата: 19.04.23 07:41
Оценка: +2
Здравствуйте, Кодт, Вы писали:

К>Ах да, кстати. Контравариантность в С++ появляется в работе с указателями на члены. И всё.

std::function еще ковариантен по возвращаемым значениям и контрвариантем по аргументам.

#include <functional>

struct foo {};
struct bar : foo {};

bar* test(const foo&)
{
    return nullptr;
}

int main() {
    std::function<const foo*(bar&)> f = test;

    bar b;
    f(b);
}
Re[3]: Изменение сигнатуры виртуальной функции
От: rg45 СССР  
Дата: 19.04.23 08:36
Оценка:
Здравствуйте, Voivoid, Вы писали:

V>std::function еще ковариантен по возвращаемым значениям и контрвариантем по аргументам.


В этом контексте можно также вспомнить std::bind, который позволяет делать редукцию списка формальных параметров:

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

#include <iostream>
#include <functional>

void test(double value)
{
    std::cout << value << std::endl;
}

int main() {

    auto f0 = std::bind(test, std::placeholders::_1);

    f0(2.71);

    std::function<void(int, double, std::string)> f = f0;

    f(42, 3.14, "Hello, World!"); // -> 3.14
}
--
Не можешь достичь желаемого — пожелай достигнутого.
Re[3]: Изменение сигнатуры виртуальной функции
От: Кодт Россия  
Дата: 19.04.23 17:45
Оценка:
Здравствуйте, Voivoid, Вы писали:

V>Здравствуйте, Кодт, Вы писали:


К>>Ах да, кстати. Контравариантность в С++ появляется в работе с указателями на члены. И всё.

V>std::function еще ковариантен по возвращаемым значениям и контрвариантем по аргументам.

Причём он не по указателям-ссылкам, а вообще по static_cast.
#pragma GCC diagnostic error "-Wconversion"
#include <functional>

struct A {};
struct B : A {};
struct C {
    operator A()const { return {}; }
};

B* fff(const A*) { return nullptr; }
B ggg(A a) { return {}; }
C hhh(A a) { return {}; };
int ttt(long) { return 0; }

int main() {
    std::function<const A*(B*)> f = fff;
    std::function<A(B)> g = ggg;
    std::function<A(C)> h = hhh;
    std::function<long(int)> t = ttt;

    B b;
    C c;
    int d = 0;
    
    f(&b);
    g(b);
    h(c);
    t(d);
}
Перекуём баги на фичи!
 
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.