Java/Scala/C++: Чому вибір overloaded метода робиться під час компіляції?

Підписуйтеся на Telegram-канал «DOU #tech», щоб не пропустити нові технічні статті.

Код:

// Java

class Parent {
}

class Derived extends Parent {
}

class SomeClass {
    void someMethod(Parent p) { System.out.println("Parent."); }
    void someMethod(Derived d) { System.out.println("Derived."); }
}

Parent a = new Derived();
(new SomeClass()).someMethod(a); // виведеться "Parent."
// Scala

class Parent {}

class Derived extends Parent {}

class SomeClass {
  def someMethod(p: Parent) = println("Parent.")
  def someMethod(d: Derived) = println("Derived.")
}

val a: Parent = new Derived()
(new SomeClass).someMethod(a) // виведеться "Parent."
// C++

class Parent
{
public:
 virtual ~Parent() { }
};

class Derived : public Parent
{
public:
 virtual ~Derived() { }
};

class SomeClass
{
public:
 void someMethod(Parent* p) { std::cout << "Parent." << std::endl; }
 void someMethod(Derived* d) { std::cout << "Derived" << std::endl; }
};

Parent* a = new Derived();

SomeClass* c = new SomeClass();
c->someMethod(a); // виведеться "Parent."

delete a;
delete c;

Які, на вашу думку, причини такого проектування мов? Тільки швидкодія у час виконання програми?
Відповідь ніби зрозуміла тільки для Scala: сумісність.

👍ПодобаєтьсяСподобалось0
До обраногоВ обраному0
LinkedIn
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter

Итак на самом деле ошибка в понимании ООП в частности полиморфизма.

Выбор метода делается на этапе исполнения и такие методы называются виртуальными и такие методы вызываются конкретную реализацию в зависимости от типа параметра только параметр выступает объектом у которого и вызывается метод.

Соотв. простой перенос someMethod в Parent/Derived решает задачу более чем полностью остальное уже вопросы архитектуры и умения пользоваться инструментами вместо того чтобы изобретать глупости.

Но это ерунда потому как интересное дальше давайте сделаем метод с двумя параметрами.

someMethod(a, b)

который будет вызывать конкретную реализацию в зависимости от типов (а) и (б) соотв.

Для простого и достаточного случая возьмём уже имеющуюся иерархию из 2-х типов соотв. 2×2 = 4 реализаций someMethod(...) увеличение количества типом просто естественным образом «раздвинет» таблицу сочетаний х на х и ничего особенно нового в сам принцип не привнесёт.

Понеслась!

class Derived;

class Parent
{
public:
    virtual ~Parent() { }

    virtual void someMethod_f1(Parent* x2) { x2->someMethod(this); }

    virtual void someMethod(Parent* x1) { std::cout << "Parent + Parent" << std::endl; }
    virtual void someMethod(Derived* x1) { std::cout << "Derived + Parent" << std::endl; }
};

class Derived : public Parent
{
public:
    virtual ~Derived() { }

    virtual void someMethod_f1(Parent* x2) { x2->someMethod(this); }

    virtual void someMethod(Parent* x1) { std::cout << "Parent + Derived" << std::endl; }
    virtual void someMethod(Derived* x1) { std::cout << "Derived + Derived" << std::endl; }
};

void someMethod(Parent* x1, Parent* x2) { x1->someMethod_f1(x2); }

int main(int argc, char *argv[])
{
    Parent* a = new Derived();
    Parent* b = new Derived();

    someMethod(a, b);

    delete a;
    delete b;

    return 0;
}

> Derived + Derived

Вуаля!

ЗЫ: другие все комбинации также работают я проверял просто как-то внезапно лень тестовый код дописывать большой и полный.

Но!

Это ещё не конец.

Эту штуку тем же ж принципом можно расширить и по числу параметров вместо 2-х 3 и так далее хотя конечно как пишет википедия они вряд ли кому-либо понадобятся такие реализации.

А вы говорите «чому?» А потому что руки! ))

хехехе в сишарпе работает:

    class Program
    {

        class Parent { }
        class Derived : Parent { }
        class SecondOne : Parent { }

        class SomeClass
        {
            void CallTheOneImpl(Parent one)
            {
                Console.WriteLine("CallTheOne TheOne");
            }

            void CallTheOneImpl(Derived one)
            {
                Console.WriteLine("CallTheOne Derived");
            }

            void CallTheOneImpl(SecondOne one)
            {
                Console.WriteLine("CallTheOne SecondOne");
            }

            public void CallTheOne(Parent one)
            {
                dynamic ddd = one;
                CallTheOneImpl(ddd);
            }
        }

        static void Main(string[] args)
        {
            var one = new Parent();
            var derived = new Derived();
            var second = new SecondOne();

            var some = new SomeClass();

            some.CallTheOne(one);
            some.CallTheOne(derived);
            some.CallTheOne(second);
        }
    }


CallTheOne TheOne
CallTheOne Derived
CallTheOne SecondOne
Press any key to continue . . .

ЗЫ: для чистоты я проверил если явно всем задать тип Parent вместо неявного var то всё равно работает.

ЗЫ: и даже:

...
        class TheThird : Parent { }
...
            Console.WriteLine("====");

            var third = new TheThird();
            some.CallTheOne(third);


===
CallTheOne TheOne

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

профитЪ.
Получите распишитесь.

Динамічний поліморфізм дає певну гнучкість, але її не завжди достатньо. Так, у цих мовах можна реалізувати дану відсутню фічу за допомогою різних хаків, як демонструється в статті про Multiple dispatch, тільки деколи ціна — більше коду і складніша архітектура.

public class A<T>
{
    public class B : A<int>
    {
        public void M()
        {
            System.Console.WriteLine(typeof(T));
        }
        public class C : B
        {
        }
    }
}
public class P
{
    public static void Main()
    {
        (new A<string>.B.C()).M();
    }
}

А що поганого в тому, що компілятор може це зробити? То навпаки, так і потрібно робити все що можливо. В Java машині є набагато страшніші речі, які інакше як твором Франкенштейна не назвеш.

Для мене єресь робити із класів інтерфейси всюди-всюди де тільки можна, бо це кошерні шаблони і все таке. Можливо й дрібничка, але коли тих дрібничок йде на десятки мільйонів — то вже гальмо.

Для мене єресь повна — робити усюди get та set методи, коли вони не потрібні. При цьому взагалі не мати примітивних швидкодіючих типів (без танців із бубоном unsafe), які б не були потомками Object і не тягнули на собі увесь гемор життєвого циклу об′єкта. Я не розумію, чому при об′єктному проектуванні не можна робити керованим лише великий об′єкт, а те що в ньому щоб могло оброблятися спеціальними засобами, функціями, але не мало власного життєвого циклу поза об′єктом.

Так само для мене відверта маячня — мати Stream виключно з об′єктів. Чому в потоці не може знаходитись що завгодно?

Ну і нарешті, для мене суцільною непоняткою є монітор мультипоточності у кожному об′єкті (тобто, граблі), замість того щоб назавжди заборонити це неподобство, і керувати потоками виключно за допомогою спеціально створених конкурентних механізмів. Це ж було очевидно ще років 20 тому. Але ж ні, не на часі, тягнуть моржа за хобот.

Відкрийте для себе мову програмування D.

Здесь согласен, язык очень неплох. Но не набрал критического уровня опенсорса, и недостаточно распиарился. Другими словами, проиграл рынку количественно. Как и многие другие кошерные вещи.

Но не набрал критического уровня опенсорса, и недостаточно распиарился

Вы хотели наверное написать «не нашел жырного спонсора, который бы проталкивал этот язык», но почему-то начали городить чушь про «опенсорность языка» --- это типа документация под GPL, или как?

Непогано, що компілятор старається генерувати код, який виконувався б швидко. Але, на мою думку, непогано було б і мати можливість робити деякі речі під час виконання.

Але, на мою думку, непогано було б і мати можливість робити деякі речі під час виконання.

Для этого есть динамический полиморфизм.

Ну в С++, кроме виртальных методов и связанным с ними динамическим полиморфизмом, все резолвится на этапе компиляции.

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

Джава тоже компилируемый язык..

Згоден.
И С# тоже компилируемый. И исходный код компилится в промежуточный байт-код, который выполняется под управлением той или иной ВМ. Как бы это и есть гравное отличие от С++, в этом плане.

Как бы это и есть гравное отличие от С++, в этом плане.

Который (си++) также компилируется в машинный код высокого уровня (читай CISC включая модные современные AVX процессорные кеши многоядерные и мультисокетные архитектуры и уж совсем виртуализацию вообще) который даже на исполнение не факт что будет выполнятся именно в том порядке в каком он скомпилирован так что «главное отличие в этом плане» в современных реалиях давно уже не катит и точно так же ж прекрасно можно написать на си++ систему с динамической типизацией (и получим пхп либо скажем пайтон или там вообще lua) только «компилируемый непосредственно в машинный код» вот и вся разница на сегодняшний день.

ЗЫ: и даже сам си++ прекрасно может быть откомпилирован в LLVM и уже оттуда непосредственно в процессор.

И исходный код компилится в промежуточный байт-код, который выполняется под управлением той или иной ВМ. Как бы это и есть гравное отличие от С++, в этом плане.

ВМ необязательна, для джавы был en.wikipedia.org/...​iki/GNU_Compiler_for_Java который компилил в нативный код например.

Потому что тип параметров является частью сигнатуры функции/метода с тем же ж успехом можно было бы б писать:

void someMethodParent(Parent* p);
void someMethodDerived(Derived* d);

Либо полностью утрируя:

void someMethodParent(void* p);
void someMethodDerived(void* d);

Либо в случае суперкласса:

void someMethodParent(Object p);
void someMethodDerived(Object d);

Більше цікаве не те, чому дозволяється мати обидва методи, а те, чому не дозволено робити вибір overloaded метода у час виконання.

Тому що це різні методу з огляду на мову зі статичною типізацією.

ОК зайдемо з іншого боку напишіть метод з параметром типу variant і буде вам щастя.

Потому что не захотели делать мультиметоды, лентяи.

stackoverflow.com/...​and-overriding-is-runtime

просто говоря, потому что компилятор может. При overload используються статические методы. Поскольку их логика написана в коде, то легко подстроить компилятор на обработку такого кода, т.к. он всегда сможет точно указать если ошибка есть.
При override он этого сделать не может, потому как не обязательно будет использован код, который есть сейчас; вдруг кто-то добавит еще одного наследника уже из другой библиотеки. При компиляции это не поймать, потому компилятор и не помогает

просто говоря, потому что компилятор может.

Чому хлопець схилив дівчину зайнятися із ним коханням? — Тому що міг:)

А якщо по темі, то так, компілятор може відловити більше помилок у випадку статичного лінкування.

ну какбы задача компилятора ловить ошибки, и он ее выполняет как может. У каждого свои задачи ;)

Чому вибір overloaded метода робиться під час компіляції?

Потому что именно компилятор реализует механизм наследования (решает какие части кода за какими выполнять) и, соответственно, устанавливает переходы в нативном коде.

Parent a = new Derived();
(new SomeClass()).someMethod(a); // виведеться «Parent.»

Потому что так работает наследование. Фактически — класс Derived это тот же Parent , только с блекджеком. Когда ты делаешь Parent a = new Derived(); — ты фактически берёшь общее от Derived и Parent, а это общее и есть Parent, именно поэтому new SomeClass()).someMethod(a); тебе и показывает что это Parent.

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

Не зовсім з вами згоден. Коли ми пишемо: Parent a = new Derived(); ми НЕ

фактически берёшь общее

, а створюємо повноцінний об`єкт типу Derived. Але вказує на цей об`єкт змінна яка гадає що вказує на об`єкт типу Parent. Відповідно коли ми передаємо її в метод .someMethod(a) компілятор бачить що ми передали змінну «типу» Parent — тому і викликає метод що працює з Parent.

Потому что в C++ (который впринципе ввел понятие виртуальной функции широко) определение типа объекта не всегда возможно. Только если включен RTTI

Ну так ж без RTTI неможливі й інші речі (dynamic_cast<>, typeid, виключення), які, незважаючи на це, є в C++.

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

„чуть” изменим код:

class Parent {
void invokeSome(SomeClass someClass) { someClass.someMethod(this); }
}

class Derived extends Parent {
void invokeSome(SomeClass someClass) { someClass.someMethod(this); }
}

class SomeClass {
void someMethod(Parent p) { System.out.println("Parent."); }
void someMethod(Derived d) { System.out.println("Derived.„); }
}

Parent a = new Derived();
a.invokeSome(new SomeClass()); // виведеться „Derived.”

Так спрацює.

Тут інше питання: чому при проектуванні Java/Scala/C++ така непослідовність: для override — одне, для overload — інше...

Для С++ причина:

Stroustrup mentions in The Design and Evolution of C++ that he liked the concept of multi-methods and considered implementing it in C++ but claims to have been unable to find an efficient sample implementation (comparable to virtual functions) and resolve some possible type ambiguity problems

en.wikipedia.org/wiki/Multiple_dispatch
Для java/scala (C# тоже) думаю что причина такая же...

Цікаво: виходить, основна причина таки в швидкодії.

Проблему можливої неоднозначності ніби можна було б вирішити викиданням виключення. Здогадуюся, маються на увазі ситуації на зразок тієї, коли передається аргументом об’єкт класу, що є наслідником двох класів, жоден з яких не наслідує іншого, а overloaded методи є тільки для кожного з цих 2 класів.

Це не непослідовність, а практичність. Тому що:
— overload та override це зовсім різні речі, хоча у коді виглядають схоже
— для overload можливо згенерувати статичний виклик і він буде виконуватись набагато швидше
— для overload методи в згенерованому коді насправді будуть мати різні назви, тобто це — лише синтаксичний цукор (так, якби ви в коді самі писали sum_int(int a){} sum_long(long a) etc.)
— для override компілятору доводиться визначати, який саме метод треба викликати і використовувати динамічні таблиці виклику

Більше цікаве порівняння overload не з override (що зводиться до визначення типу об’єкта, на якому викликається метод, під час виконання), а з визначенням типів аргументів, що передаються у метод, під час виконання. Але, здається, ці 4 твердження можна сказати і для такого порівняння. Доповнивши, що, за інформацією від Страуструпа, швидкодія у таких, як підказали Yegor Chumakov і Oleksandr Manenko, мультиметодів ще гірша, ніж у віртуальних функцій.

Доповнивши, що, за інформацією від Страуструпа, швидкодія у таких, як підказали Yegor Chumakov і Oleksandr Manenko, мультиметодів ще гірша, ніж у віртуальних функцій.

Що саме власне заважає у цьому місці з цією метою використати саме механізм віртуальних функцій?

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

А вы хотите именно вместо косвенного вызова именно свич с конкретными вызовами.

Parent* a = new Derived();

SomeClass* c = new SomeClass();

switch (realtypeof(a)) {
case realtypeisParent:
    c->someMethod((Parent*)a); // виведеться "Parent."
    break;
case realtypeisDerived:
    c->someMethod((Derived*)a); // виведеться "Derived."
    break;
default:
    throw realtypeexception(noParentnoDerived);
    break;
}

Приблизно так, за виключенням того, що default-гілка зайва, бо об’єкт a вже вдалося створити, тобто метод на об’єкті a має реалізацію.

/// C++

class Parent
{
public:
 virtual ~Parent() { }
};

class Derived : public Parent
{
public:
 virtual ~Derived() { }
};

class TheSecondOne : public Parent
{
public:
 virtual ~TheSecondOne() { }
};

class SomeClass
{
public:
 void someMethod(Parent* p) { std::cout << "Parent." << std::endl; }
 void someMethod(Derived* d) { std::cout << "Derived" << std::endl; }
};

Parent* a = new TheSecondOne();

switch (realtypeof(a)) {
case realtypeisParent:
    c->someMethod((Parent*)a); // виведеться "Parent."
    break;
case realtypeisDerived:
    c->someMethod((Derived*)a); // виведеться "Derived."
    break;
default:
    throw realtypeexception(noParentnoDerived); // oops we do not really handle that fucking AnotherOne so bad so sad so fucking somebody so-called "architect" stupid just go and kill him kill them all they are stupids welcome to the real world Neo (c)(tm)
    break;
}
delete a;
delete c;

А, ну саме в цьому коді треба. Я мав на увазі, що якщо мовою підтримувався б вибір методу на основі динамічних типів аргументів, то не треба було би писати свій метод для кожного нащадка Parent: ті нащадки Parent, для яких обробника не було б, оброблялися б методом, відповідним найближчому для них предку.

ті нащадки Parent, для яких обробника не було б, оброблялися б методом, відповідним найближчому для них предку.

Звідки це випливає чи має випливати? І що таке «відповідним найближчим для них предку»? І що робити у випадку множинного наслідування? І що робити коли якраз методу до предка нема а є лише методи до inherited типів? Видавати помилку компіляції?

Звідки це випливає чи має випливати?

Не випливає, але так було б логічно спроектувати мову, додержуючись логіки, яка зараз є: якщо клас SomeClass містить тільки метод someMethod(p: Parent), то, визначивши val a: Derived = new Derived(), результатом виконання (new SomeClass).someMethod(a) є виклик саме цього методу.

І що таке «відповідним найближчим для них предку»?

Якщо B наслідує A, C наслідує B, є тільки метод з аргументом типу A і метод з аргументом типу B, то для (new SomeClass).someMethod(a) логічно було б викликати другий метод.

І що робити у випадку множинного наслідування?

У випадку неоднозначності через множинне наслідування можна було б викидати виключення під час виконання. На момент компіляції динамічний тип об’єкта, взагалі кажучи, невідомий.

І що робити коли якраз методу до предка нема а є лише методи до inherited типів? Видавати помилку компіляції?

Так, помилку компіляції, бо методи до inherited типів можуть використовувати члени, що з’являються саме в цих inherited типах.

Так, помилку компіляції, бо методи до inherited типів можуть використовувати члени, що з’являються саме в цих inherited типах.

А ну ок тобто мова йде про віртуальну функцію яка не є членом класу але приймає екземпляр класу у якості параметру а навіщо?

з визначенням типів аргументів, що передаються у метод, під час виконання

Чесно кажучи, ніколи не чув про такі реалізації в компіляторних мовах программування.

Это приведение типа. Если бы такой вызов (с явным указанием типа) работал полиморфно — как бы тогда вызвать метод приведенного типа?

Не розумію. Приклад коду?

Пример с приведением типа ты привёл:

«Parent* a = new Derived();
SomeClass* c = new SomeClass();
c->someMethod(a); // виведеться „Parent.“»

Пример без приведения к конкретному типу (т.е. динамический полиморфизм) выглядел бы так:

«class SomeClass
{
public:
void someMethod(Parent* p) { std::cout << p->getName() << std::endl; }
};»
(для виртуальной функции getName())

Без перегрузки функции, был бы вызов с динамическим полиморфизмом.

Тепер зрозумів. Так, тоді при передачі в someMethod об’єкта a, визначеного так:

Parent* a = new Derived();

для коду p->getName() логічно було б викликати реалізацію з Derived.

Викликати всередині someMethod реалізацію з Parent можна було б так само, як її можна викликати в області створення a:

p->Parent::getName();

але якщо така потреба виникла б, був би незрозумілий дизайн класів Parent і Derived, тобто навіщо оголошувати функцію getName() віртуальною.

А як в рантаймі ambiguous function call розрулювати? І зрозуміліше, що код робить.

Розрулювати можна було б викиданням виключення:)

Так, код зрозуміліший. І водночас менш гнучкий:)

Підписатись на коментарі