LINUX.ORG.RU

Давайте поговорим о трейтах

 , , , ,


0

2

Вот я смотрю очередной раз на трейты. Первоистчник говорит:

Traits do not specify any state variables, and the methods provided by traits never directly access state variables.

Далее смотрим первую попавшуюся статью по PHP и видим:

trait Id
{
    protected $id;
    public function getId()
    {
        return $this->id;
    }
}

Т.е., в PHP трейты - это не трейты, я верно понял?

И теперь вот что я хотел узнать - как соотносятся по производительности трейты с одиночным наследованием реализации. Тут я слегка упираюсь в то, что я не понимаю, как реализованы вирт. ф-ии в С++ при множественном наследовании. Как я понял, если класс Child наследует от Parent1 и Parent2, и мы хотим вызвать виртуальный метод Parent1::Method, то мы должны где-то найти указатель на этот метод. Метод ищется в VMT и Child содержит 2 VMT, для каждого из Parent.

И получается, чтобы найти этот VMT, нам нужно сначала вызвать некую ф-ю VMTOffsetOfParent1(Child). Эта функция может вызываться для всех потомков Parent1, и может принимать на вход только какое-то число, идентификатор класса, а возвращать она должна указатель. Такое отображение можно реализовать только с помощью хеш-таблицы, b-дерева или иного объекта с логарифмическим временем доступа по отношению к количеству потомков Parent1.

По сравнению с этим, при одиночном наследовании вызов вирт.метода осуществляется за линейное время.

Если Parent1 и Parent2 становятся трейтами, то опять же нам нужно вызвать функцию GetParent(Child), которая опять же требует хеш-таблицы для своей реализации, со временем доступа, логарифмическим по отношению к количеству классов, воплощающих этот трейт.

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

Наверное, я зря сюда приплёл вирт. ф-ии, поскольку затратная операция здесь - скорее dynamic_cast. Вот нашёл статейку, как подобрать хеш-функцию, чтобы не было конфликтов между классами.

Верно я мыслю или нет? В свете этого, верно ли, с точки зрения производительности, решение отказаться от наследования реализации, принятое в Rust и Golang?

А зачем мне это нужно: нужно быстро придумать, как реализовать ООП в Яре. Понятно, что нужно включить минимум для начала (потому что версия только 0.4). Но любой минимум потом оказывается более, чем минимальным, и включив что-то в язык, потом уже не избавишься. Поэтому тэг «Яр» здесь должен стоять, уж извините. Он особенно хорош тем, что те, кому я надоел, ставят этот тег в список игнорируемых.

★★★★★

Последнее исправление: den73 (всего исправлений: 2)

Смешивать множественное наследование и виртуальные функции в с++ - это ад. В большинстве случаев оно вообще не соберётся, а если и соберётся, то могут возникнуть забавные казусы.

RazrFalcon ★★★★★
()

И получается, чтобы найти этот VMT, нам нужно сначала вызвать некую ф-ю VMTOffsetOfParent1(Child).

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

xaizek ★★★★★
()
Ответ на: комментарий от xaizek

Да, стоимость сосредотачивается в dynamic_cast, но он сам по себе недёшев, и столько же стоит получение трейта (если я всё правильно понял).

А при одиночном наследовании реализации всё получается дёшево (за пару операций с указателями).

Если я это правильно понимаю, то трейты не есть верх совершенства и в плане производительности они - шаг назад по сравнению с виртуальными методами при одиночном наследовании.

Вот я и хочу понять, правильно ли я это понимаю.

den73 ★★★★★
() автор топика
Ответ на: комментарий от no-such-file

Трейты в пыхе это миксины.

Спасибо! Меня не перестаёт поражать, насколько плохо обстоит дело с терминологией.

den73 ★★★★★
() автор топика
Ответ на: комментарий от RazrFalcon

Смешивать множественное наследование и виртуальные функции в с++ - это ад.

Я не имею в виду конфликты наследования (ромб и т.п.), я имею в виду, что два предка имеют свои методы. Мне кажется, что тут не должно быть проблем.

den73 ★★★★★
() автор топика
Ответ на: комментарий от den73

dynamic_cast есть только, если пользователь его сам написал. Компилятор его не вставляет. Так что для абсолютного числа операций его цена роли не играет.

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

xaizek ★★★★★
()
Последнее исправление: xaizek (всего исправлений: 1)

Ты прочитал частное и пытаешься распространить это на общее? Думаю, в общем случае трейты — это то же самое что миксины, в некоторых языках у миксин были некоторые ограничения, а трейты — это такие миксины в стиле JS без ограничений. Соответственно, в норме трейты могут иметь полный доступ к локальному состоянию. Анальные ограничения частных случаев в языках нас не должны интересовать.

Впрочем, что то и другое в качестве суррогата множественного наследования малопригодно в динамических языках, они имитируют только квазинаследование в стиле плюсов

portquest2016
()
Ответ на: комментарий от den73

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

ddos3
()
Последнее исправление: ddos3 (всего исправлений: 1)
Ответ на: комментарий от den73

Нет там никакой дополнительной стоимости по сравнению с единичным наследованием, и никакого dynamic_cast тоже нет. Ссылки на виртуальные методы точно так же вынимаются из VTable, который инициализируется при создании объекта; ссылки на не-виртуальные методы резолвятся еще во время компиляции.

dynamic_cast, сам по себе недёшев

Откуда дровишки? Ссылка на тип вынимается из VTable, в общей сложности два чтения из памяти всего. Потом еще сравнение типов и приведение указателей, но это совсем копейки.

ddos3
()
Ответ на: комментарий от xaizek

dynamic_cast есть только, если пользователь его сам написал.

Ну вроде да, всё «чисто», если поменьше писать этот dynamic_cast.

Но в такое время суток слабО мне осилить trait objects в Расте и как его реализация соотносится с реализацией множественного наследования в С++. Видимо, придётся в другой раз...

den73 ★★★★★
() автор топика
Ответ на: комментарий от ddos3

dynamic_cast, сам по себе недёшев

Откуда дровишки?

Пост читал? Такая фамилия, как Страуструп, тебе о чём-нибудь говорит? Вот он-то дровишки и подогнал.

den73 ★★★★★
() автор топика
Ответ на: комментарий от den73

Пост читал? Такая фамилия, как Страуструп, тебе о чём-нибудь говорит? Вот он-то дровишки и подогнал.

Страуструп, напротив, говорил, что dynamic_cast можно сделать очень эффективно, заверяя, что «никаких дорогостоящих сравнений строк не требуется» :-) Почти что цитата из его TC++PL :-)

anonymous
()
Ответ на: комментарий от anonymous

Когда я прочитал где-то про сравнения строк, чуть не упал. Очевидно же любому лисперу, что надо запихнуть иды классов (числа или адреса неких структур) в хеш-таблицу. Но без хеш-таблицы или иного словаря я не вижу, как сделать dynamic_cast. То, что написано в статье по ссылке, это изыск, фактически там просто подбирается способ нумерации классов таким образом, чтобы хеш-функция не имела конфликтов и в каждом ведре было гарантированно по одному элементу. И это опирается на полный анализ всей программы (полный список классов должен быть известен во время генерации ид-ов класса). Во всяком случае, я так понял.

den73 ★★★★★
() автор топика
Последнее исправление: den73 (всего исправлений: 3)
Ответ на: комментарий от den73

Когда я прочитал про сравнения строк, чуть не упал. Очевидно же любому лисперу, что надо запихнуть иды классов (числа или адреса неких структур) в хеш-таблицу.

Ты когда-нибудь видел как создаются таблицы виртуальных функций вручную на C? :-) И как на C можно реализовать наследование? :-) Иными словами, ты видел что происходит под капотом у DSL, под названием C++? :-) Там всё банально, но т.к. руками писать каждый раз vtbl и наследование утомительно и чревато, придумали DSL с ключевыми словами virtual :-)

anonymous
()
Ответ на: комментарий от anonymous

Ты когда-нибудь видел как создаются таблицы виртуальных функций вручную на C?

Примерно понятно, как. Вот я и думаю, а может и не нужны трейты вообще?

den73 ★★★★★
() автор топика

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

Legioner ★★★★★
()
Ответ на: комментарий от Legioner

Понятно. Это скорее сахар.

den73 ★★★★★
() автор топика
Ответ на: комментарий от den73

Чтобы было более ясно и не возникало лишних обсуждений о стоимости dynamic_cast, слова самого Страуструпа из статьи, ссылка на которую дана в посте:

Dynamic casting is useful in many applications, but current implementations of this functionality are slow compared with other C++ operations. Current implementations of dynamic cast require extra type information to be kept for each class with a virtual function. When dynamic cast is called, the algorithm will traverse a data structure representing all of Actual’s base classes, searching for a match for the Target type.

Т.е. ясно сказано: требуется поиск в некоей структуре данных, а не какое-то конечное количество действий. Поиск - это как минимум логарифм, если без изысков типа как в той статье. По сравнению с этим, просто вызов виртуального метода требует фиксированного числа действий, что при одиночном, что при множественном наследовании.

Но если метод виртуальный, а переменная у нас не того класса, а предка, то сначала надо сделать dynamic_cast, который и имеет стоимость.

А в go/rust нужно в этой же ситуации получить трейт - это имеет примерно ту же стоимость.

Короче, надо в Яр просто включить структуры с одиночным наследованием и родовые функции из лиспа, и сказать, что это универсальные, но медленные родовые функции, но вы можете реализовать свои VMT. Всё равно никто пользоваться не будет и можно не бояться зоопарка из конкурирующих реализаций ООП.

den73 ★★★★★
() автор топика
Последнее исправление: den73 (всего исправлений: 1)
Ответ на: комментарий от den73

Примерно понятно, как. Вот я и думаю, а может и не нужны трейты вообще?

Мне примерно не понятно, чего ты хочешь сделать? :-)

anonymous
()
Ответ на: комментарий от den73

Ну в общем, выше тебе уже ответили. Той статье больше 10 лет уже, с тех пор кое-что поменялось, да и тогда было не так уж плохо. Понятно, что dynamic_cast будет дороже, чем static_cast, но не на столько, чтобы это когда-нибудь стало проблемой.

ddos3
()
Ответ на: комментарий от anonymous

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

den73 ★★★★★
() автор топика
Ответ на: комментарий от ddos3

Извини, но ты неубедителен совсем. Хотя можешь попробовать дать пруфлинк.

den73 ★★★★★
() автор топика
Ответ на: комментарий от den73

Я хочу не сделать лишнего на раннем этапе развития языка, чтобы потом локти не обкусать.

Создатель Clojure локти не кусает, потому что не замарачивается обратной совместимостью :-) Ты же не пишешь «Си с классами», так что не парься :-) На данном этапе важно довести до конца хоть какой-то вариант языка :-) И чем быстрее, тем лучше :-)

anonymous
()
Ответ на: комментарий от den73

current implementations of this functionality

Больше 10 лет прошло. Кое-что поменялось уже.

are slow compared with other C++ operations

Именно что по сравнению с другими операциями в С++, а не просто «дорого».

требуется поиск в некоей структуре данных

Да, если есть иерархия A->B->C, у нас указатель на А, который мы хотим привести к В, а объект на самом деле С, то придется проверить список предков объекта, чтобы убедиться, что В в нем присутствует. То, что я описывал, это самый частый случай, когда мы приводим указатель к типу, который соответствует объекту (и выпадаем из проверки на первой же итерации). Но в любом случае, количество итераций - не больше, чем предков у С, то есть единицы в подавляющем большинстве случаев. Современный процессор это схавает быстрее, чем один cache-miss. Городить тут какие-то сложные структуры данных с логарифмическим поиском уже давно не релевантно.

класс не тот

Что это значит? Все равно все ссылки на методы в одном vtable лежат, на который есть ссылка из объекта.

ddos3
()
Ответ на: комментарий от anonymous

Основной вопрос на самом деле: не промазали ли создатели Раста с Го, что убрали одиночное наследование реализации? И я пока не приблизился к разгадке, хотя кое-что узнал про множественное наследование в С++. Думается, далеко не все в отрасли ещё осознали и приняли эти трейты (даже создали Раста ещё не совсем их доделали). Лично мне кажется, что они лучше, чем тот ад, который в С++, но я не совсем уверен в них - опыта работы с ними нет. А одиночное наследование реализации есть в Яве и Delphi, так что это чисто маркетинговая вещь. Но если я пойму, что оно точно не нужно, то не стану включать.

den73 ★★★★★
() автор топика
Ответ на: комментарий от RazrFalcon

Смешивать множественное наследование и виртуальные функции в с++ - это ад. В большинстве случаев оно вообще не соберётся, а если и соберётся, то могут возникнуть забавные казусы.

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

#include <iostream>

int main()
{
    struct B1 {
        virtual void foo() { std::cout << "B1::foo" << std::endl; }
    };

    struct B2 {
        virtual void foo() { std::cout << "B2::foo" << std::endl; }
    };

    struct C1 : B1, B2 {
        void foo() override { std::cout << "C1::foo" << std::endl; }
    };

    C1 c1;
    B1& rb1{c1};
    B2& rb2{c1};
    
    c1.foo();
    rb1.foo();
    rb2.foo();
}
asaw ★★★★★
()
Последнее исправление: asaw (всего исправлений: 1)
Ответ на: комментарий от anonymous

На самом деле неплохо было бы определится еще и с тем зачем он вообще нужен, каковы его непосредственные задачи и сфера применения:) Если это прояснится(не считая того варианта, что я хочу создать универсальный и рулезный во всех отношениях язык, превосходящий все что есть, и все что было создано до меня) то дела пойдут, очевидно, быстрей. Страуструпу было гораздо проще в этом плане, он создавал ЯП для себе подобных мартышек, да и обстоятельства ему помогли, в то время ярлык ООП навешивали на все что шевелится, он попал в тренд:)

portquest2016
()
Ответ на: комментарий от den73

В c++ хитрости начинаются только с виртуальным наследованием. Там и указатель на parent в vtable появляется и заход в вирт метод через промежуточный трамплин, корректирующий адрес this. Но даже в этом случае dynamiccast не используется. Там только коррекция указателя this

i36_zubov
()
Ответ на: комментарий от asaw

У товарища не соберется и казусы возникнут, что не ясно то? Не может он без казусов

i36_zubov
()
Ответ на: комментарий от den73

Ты можешь сделать интерфейсы как пары указателей на this и на vtable. Тем самым можно будет дополнять методами любую структуру

i36_zubov
()
Ответ на: комментарий от den73

множественное наследование в С++

Мн. наследование вообще - это важный приём в объектно-ориентированном проектировании :-) И Страуструп был изначально убеждён в том, что основной упор должен делаться именно на проектирование :-) Поэтому и было принято решение в введении мн. наследования в C++ :-)

А одиночное наследование реализации есть в Яве и Delphi,

Т.к. мн. наследование (и вообще, ООП) относится чисто к проектированию, в Java пошли иным путём - наследование одиночное, а реализация интерфейсов - множественное :-) И это решило много проблем, в частности, с дупликацией наследуемых объектов, что в C++, иной раз, создаёт много-много проблем и головоломок :-)

Плюс в Java все классы полиморфны, т.е. методы - виртуальны, в отличии от C++ :-) На мой взгляд, в Java сделали лучше, чем в C++ :-)

anonymous
()
Ответ на: комментарий от ddos3

Что это значит? Все равно все ссылки на методы в одном vtable лежат, на который есть ссылка из объекта.

Переменная, в которой пришёл класс, если не та, и нужен dynamic_cast, то эти единицы, сколько бы их не было, придётся обработать. При множественном наследовании их может стать не так уж мало. Пока будешь ходить по дереву, можно словить, например, лишний cache-miss. Так что более сложное осталось более сложным, чем простое, и через 10 лет, и навсегда останется. И dynamic_cast навсегда останется более сложным, чем вызов вирт. метода.

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

Все равно все ссылки на методы в одном vtable лежат, на который есть ссылка из объекта.

Это не так уж и просто при множественном наследовании, но вроде бы все проблемы сосредоточены в dynamic_cast, ну и плюс дублирование VMT.

den73 ★★★★★
() автор топика
Ответ на: комментарий от anonymous

На мой взгляд, в Java сделали лучше, чем в C++ :-)

История повторяется дважды: первый раз в виде трагедии, второй — в виде фарса.

portquest2016
()
Ответ на: комментарий от portquest2016

то дела пойдут, очевидно, быстрей

И это, пожалуй, самое главное :-) За что я не люблю цепепе, так это за то, что «быстрей» на нём не получается при всём желании, особенно, при отсутствии опыта :-) Пока наешься всё вокруг изменится сто раз :-)

Страуструпу было гораздо проще в этом плане, он создавал ЯП для себе подобных мартышек, да и обстоятельства ему помогли

Ему очень сильно повезло в том, что вокруг было много толковых людей, с которыми он имел возможность непосредственно общаться, получать дельные советы :-) Ну и упорства ему не занимать :-)

anonymous
()
Ответ на: комментарий от i36_zubov

Да, это хороший вариант, в стиле Си. Мне нравится. Я его имел в виду. Но создание более высокоуровневых конструкций для ООП неизбежно. Вопрос лишь в том, стоит ли сдирать с Раста/Го или они ещё сами не поняли, что у них получилось. Пропаганду я почитал, конечно, но это пропаганда, наверное, у Гугла сидят блоггеры на бюджете, писать статьи на тему «как я через год осознал, что Го идеален».

den73 ★★★★★
() автор топика
Ответ на: комментарий от anonymous

в Java пошли иным путём - наследование одиночное, а реализация интерфейсов - множественное

Но Ява медленнее плюсов.

den73 ★★★★★
() автор топика
Ответ на: комментарий от den73

Но Ява медленнее плюсов.

Но безопаснее, проще в изучении, проще и приятнее в работе :-) Это не фанбойская агитация, это факт :-)

И Лисп медленнее плюсов :-) Но ты же, почему-то, предпочитаешь Лисп? :-) Так и миллионы программистов создают полезный софт на Java, в т.ч. и IDE для C++ :-)

anonymous
()
Ответ на: комментарий от den73

это, опять же, касается вопроса, о назначении языка, в частности для его целевой аудитории, вопрос о том, должен ли язык реализовывать ООП, и в каком виде, или он должен содержать специальные конструкции, которые как-бы намекают пользователю, что, типа, «я ооп, ведь у меня есть defineClass! Имей это в виду!».

portquest2016
()
Ответ на: комментарий от den73

Итак, выводы на сегодня

  • dynamic_cast - медленная операция независимо от множественного наследования
  • в случае, когда dynamic_cast не нужен, множественное наследование не вносит дополнительного замедления, в т.ч. на вызов вирт. метода.
den73 ★★★★★
() автор топика
Ответ на: комментарий от anonymous

А Rust не медленнее плюсов и при этом безопаснее их. Про простоту не скажу, хотя возможно, что он и проще плюсов в использовании.

den73 ★★★★★
() автор топика
Ответ на: комментарий от portquest2016

Это, опять же, касается вопроса, о назначении языка

Конечно же, я хочу язык, идеальный во всех отношениях :) Да, я знаю, что так не бывает, но я по возможности прорабатываю все аспекты, чтобы знать шкалу для выбора компромиссов.

Что касается конкретно ООП, то С++ мне активно не нравится, CLOS тоже не особо. Я обычно использую структуры и родовые функции в лиспе. Как минимум, я хочу объектную систему, к которой не будет нареканий по скорости.

Я вижу, что народ отверг одиночное наследование реализации и ударился в трейты. Если это была ошибка, у меня есть шанс на конкурентное преимущество в борьбе с Rust и Go. Вот это и предстоит выяснить на данном этапе. Поскольку с т.з. проектирования мне это мало понятно, я хотя бы пытаюсь понять с т.з. производительности, потому что CLOS конкретно тормозной и на это многие жалуются.

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

den73 ★★★★★
() автор топика

скорости не в языках, а главным образом в технологиях и подходах - можно например создать 1000 объектов, а можно единицы и насчет трейтов man abstract methods

anonymous
()
Ответ на: комментарий от den73

Как минимум, я хочу объектную систему, к которой не будет нареканий по скорости.

В сегодняшних условиях,когда ярлык «ООП» навешивается на все что не попадя, это означает то, что у тебя не будет настоящего ООП, потому что динамическое ООП в стиле смолтока никогда не выиграет у статического квазиооп на синтетических тестах. Естественно, для большинства приложений это чистая иллюзия, так как основные тормоза в системе идут из-за синхронности — простоя приложения в ожидании результата вычислений, поэтому любой супербыстрый язык, на деле вовсе не быстрый, а быстрый только на отдельных конструкциях. Но это никого не волнует на самом деле, культура совершенно другая.

portquest2016
()
Ответ на: комментарий от den73

Переменная, в которой пришёл класс, если не та, и нужен dynamic_cast, то эти единицы, сколько бы их не было, придётся обработать. При множественном наследовании их может стать не так уж мало. Пока будешь ходить по дереву, можно словить, например, лишний cache-miss.

Вот как может человек, придумывающий язык, думать о реализации, где кто-то будет ходить по дереву? Есть объект с идентификатором типа, dynamic_cast отвечает на вопрос «можно ли привести тип с этим идентификатором к типу с требуемым?». Как на него легче всего ответить? Взять и посмотреть в таблицу, составленную на этапе компиляции. Даже если одна иерархия насчитывает сотни типов, эта таблица будет небольшая. Так её ещё и можно оптимизировать, и помещать туда только те типы, которые потенциально могут приводиться друг к другу.

asaw ★★★★★
()
Ответ на: комментарий от den73

Я хочу не сделать лишнего на раннем этапе развития языка, чтобы потом локти не обкусать

при этом

месяцами разбираться я тоже не хочу

Таки выбери что-то одно :(

Алсо, советую посмотреть, как оно устроено в D :)

jeuta ★★★★
()
Ответ на: комментарий от asaw
int main()
{
    struct B1 {
        virtual void foo() = 0;
    };

    struct B2 : B1 {
        virtual void bar() = 0;
    };

    struct C1 : B1 {
        void foo() { qDebug() << "C1::foo"; }
    };

    struct C2 : C1, B2 {
        void bar() { qDebug() << "C1::bar"; }
    };

    C2 c2;
}

Упс:

error: variable type 'C2' is an abstract class
    C2 c2;
       ^
note: unimplemented pure virtual method 'foo' in 'C2'
        virtual void foo() = 0;
                     ^

RazrFalcon ★★★★★
()
Последнее исправление: RazrFalcon (всего исправлений: 2)
Вы не можете добавлять комментарии в эту тему. Тема перемещена в архив.