Классический полиморфизм и std::variant (C++17).
Интересная статья натолкнула на попытку сравнения производительности кода, использующего старый, основанный на наследовании и виртуальных функциях полиморфизм и новый подход, использующий std::variant, появившийся в стандарте C++17.
Итак имеем типы, связанные наследованием от общего предка - абстрактного класса:
struct ShapeAbstract
{
virtual void Print() = 0;
virtual double Area() = 0;
virtual ~ShapeAbstract() {};
};
struct CircleVirt : ShapeAbstract
{
CircleVirt(double val) : radius(val) {}
void Print() override { cout << "CircleVirt. " << "Radius: " << radius << endl; }
double Area() override { return 3.14 * radius * radius; }
double radius;
};
struct SquareVirt : ShapeAbstract
{
SquareVirt(double val) : side(val) {}
void Print() override { cout << "SquareVirt. Side: " << side << endl; }
double Area() override { return side * side; }
double side;
};
struct EquilateralTriangleVirt : ShapeAbstract
{
EquilateralTriangleVirt(double val) : side(val) {}
void Print() override { cout << "EquilateralTriangleVirt. Side: " << side << endl; }
double Area() override { return (sqrt(3) / 4) * (side * side); }
double side;
};
Создадим контейнер и поместим к него указатели на объекты этих типов
vector<ShapeAbstract*> shapes_abstr;
for(int i =0;i<SIZE;i++)
{
shapes_abstr.emplace_back(new EquilateralTriangleVirt { 5.6 });
shapes_abstr.emplace_back(new SquareVirt { 8.2 });
shapes_abstr.emplace_back(new CircleVirt { 3.1 });
}
Теперь, поскольку имеем общего предка - можем вызывать виртуальные функции по указателю на базовый класс. Будет производится вызов переопределенной функции конкретного класса.
for (ShapeAbstract* shape: shapes_abstr)
{
shape->Area();
}
За исключением появившегося в С++11 вида for (а также 'emplace_back' и 'override')- тут всё старо, как Страуструп (долгих лет ему).
В случае аллергии на "сырые" указатели и прогрессирующий склероз, приводящий к утечкам компьютерной памяти можно прописать std::shared_ptr.
vector<shared_ptr<ShapeAbstract>> shapes_shared;
for(int i =0;i<SIZE;i++)
{
shapes_shared.emplace_back(make_shared<EquilateralTriangleVirt>(10.4));
shapes_shared.emplace_back(make_shared<SquareVirt>(8.2));
shapes_shared.emplace_back(make_shared<CircleVirt>(3.1));
}
Обернули сырой указатель в shared_ptr, который, в частности, берет на себя заботу о приведении к конкретному типу. Полиморфное поведение, конечно, сохранилось.
for (shared_ptr<ShapeAbstract> shape: shapes_shared)
{
shape->Area();
}
За красивую обертку придется заплатить производительностью во время выполнения. По проведенным мной измерениям (цифры разнятся и не претендуют на точность, поэтому приводить не буду) - время заполнения контейнера в приведенном выше коде увеличивается в 1,5 раза, а выполнение виртуальных функций Area() для всех элементов контейнера - возрастает в 2-3 раза.
При отказе от наследования и виртуальных функций всё рушится. Разместить несвязанные типы в общем векторе не удастся и никакого полиморфного поведения не предвидится.
С++17 предлагает новый библиотечный тип std::variant. Если кто помнит работу с COM на С++ и union с аналогичным названием, то может вздрогнуть при этой новости. Но не всё так плохо, как считают некоторые пессимисты.
Представим себе типы, не связанные иерархией наследования
struct CircleVar
{
void Print() { cout << "CircleVar. " << "Radius: " << radius << endl; }
double Area() { return 3.14 * radius * radius; }
double radius;
};
struct SquareVar
{
void Print() { cout << "SquareVar. Side: " << side << endl; }
double Area() { return side * side; }
double side;
};
struct EquilateralTriangleVar
{
void Print() { cout << "EquilateralTriangleVar. Side: " << side << endl; }
double Area() {
return (sqrt(3) / 4) * (side * side);
}
double side;
};
Определим variant, который может содержать указанные типы
using ShapeVar = variant<CircleVar, SquareVar, EquilateralTriangleVar>;
Для работы std::visit нам потребуется Visitor. Реализуем его так
template <class... Func>
struct Visitor : Func...
{
using Func::operator()...;
};
template <class... Func> Visitor(Func...) -> Visitor <Func...>;
Создаем контейнер с std::variant.
vector<ShapeVar> shapes_var;
Заполняем его
for(int i =0;i<SIZE;i++)
{
shapes_var.emplace_back(EquilateralTriangleVar { 5.6 });
shapes_var.emplace_back(SquareVar { 8.2 });
shapes_var.emplace_back(CircleVar { 3.1 });
}
Хм... Время заполнения вдвое меньше - чем время заполнения в случае "сырых" указателей, ведь дополнительная память не выделяется.
Вызываем функции объектов в контейнере с использованием std::visit, приведенного выше Visitor и определенных "по месту" лямбда-функций, которые манипулируют функциями объектов желаемым образом.
for (ShapeVar& shape: shapes_var)
{
visit(Visitor{
[](CircleVar& c) { cout << c.Area(); },
[](SquareVar& c) { c.Print(); },
[](EquilateralTriangleVar& c) { c.Area(); }
}, shape);
}
Время вызова тоже вдвое меньше - чем время вызова виртуальных функций в случае "сырых" указателей.
Итак - в производительности при использовании std::variant мы выигрываем. По читабельности кода - можно прочитать статьи по ссылкам, приведенным выше. Несвязанность типов внутри std::variant - это несомненный плюс. Поддержка кода - в случае добавления/изменения типов - это вопрос требующий осмысления.
See more "Another polymorphism", "Inheritance vs std::variant"
Итак имеем типы, связанные наследованием от общего предка - абстрактного класса:
struct ShapeAbstract
{
virtual void Print() = 0;
virtual double Area() = 0;
virtual ~ShapeAbstract() {};
};
struct CircleVirt : ShapeAbstract
{
CircleVirt(double val) : radius(val) {}
void Print() override { cout << "CircleVirt. " << "Radius: " << radius << endl; }
double Area() override { return 3.14 * radius * radius; }
double radius;
};
struct SquareVirt : ShapeAbstract
{
SquareVirt(double val) : side(val) {}
void Print() override { cout << "SquareVirt. Side: " << side << endl; }
double Area() override { return side * side; }
double side;
};
struct EquilateralTriangleVirt : ShapeAbstract
{
EquilateralTriangleVirt(double val) : side(val) {}
void Print() override { cout << "EquilateralTriangleVirt. Side: " << side << endl; }
double Area() override { return (sqrt(3) / 4) * (side * side); }
double side;
};
Создадим контейнер и поместим к него указатели на объекты этих типов
vector<ShapeAbstract*> shapes_abstr;
for(int i =0;i<SIZE;i++)
{
shapes_abstr.emplace_back(new EquilateralTriangleVirt { 5.6 });
shapes_abstr.emplace_back(new SquareVirt { 8.2 });
shapes_abstr.emplace_back(new CircleVirt { 3.1 });
}
Теперь, поскольку имеем общего предка - можем вызывать виртуальные функции по указателю на базовый класс. Будет производится вызов переопределенной функции конкретного класса.
for (ShapeAbstract* shape: shapes_abstr)
{
shape->Area();
}
За исключением появившегося в С++11 вида for (а также 'emplace_back' и 'override')- тут всё старо, как Страуструп (долгих лет ему).
В случае аллергии на "сырые" указатели и прогрессирующий склероз, приводящий к утечкам компьютерной памяти можно прописать std::shared_ptr.
vector<shared_ptr<ShapeAbstract>> shapes_shared;
for(int i =0;i<SIZE;i++)
{
shapes_shared.emplace_back(make_shared<EquilateralTriangleVirt>(10.4));
shapes_shared.emplace_back(make_shared<SquareVirt>(8.2));
shapes_shared.emplace_back(make_shared<CircleVirt>(3.1));
}
Обернули сырой указатель в shared_ptr, который, в частности, берет на себя заботу о приведении к конкретному типу. Полиморфное поведение, конечно, сохранилось.
for (shared_ptr<ShapeAbstract> shape: shapes_shared)
{
shape->Area();
}
За красивую обертку придется заплатить производительностью во время выполнения. По проведенным мной измерениям (цифры разнятся и не претендуют на точность, поэтому приводить не буду) - время заполнения контейнера в приведенном выше коде увеличивается в 1,5 раза, а выполнение виртуальных функций Area() для всех элементов контейнера - возрастает в 2-3 раза.
При отказе от наследования и виртуальных функций всё рушится. Разместить несвязанные типы в общем векторе не удастся и никакого полиморфного поведения не предвидится.
С++17 предлагает новый библиотечный тип std::variant. Если кто помнит работу с COM на С++ и union с аналогичным названием, то может вздрогнуть при этой новости. Но не всё так плохо, как считают некоторые пессимисты.
Представим себе типы, не связанные иерархией наследования
struct CircleVar
{
void Print() { cout << "CircleVar. " << "Radius: " << radius << endl; }
double Area() { return 3.14 * radius * radius; }
double radius;
};
struct SquareVar
{
void Print() { cout << "SquareVar. Side: " << side << endl; }
double Area() { return side * side; }
double side;
};
struct EquilateralTriangleVar
{
void Print() { cout << "EquilateralTriangleVar. Side: " << side << endl; }
double Area() {
return (sqrt(3) / 4) * (side * side);
}
double side;
};
Определим variant, который может содержать указанные типы
using ShapeVar = variant<CircleVar, SquareVar, EquilateralTriangleVar>;
Для работы std::visit нам потребуется Visitor. Реализуем его так
template <class... Func>
struct Visitor : Func...
{
using Func::operator()...;
};
template <class... Func> Visitor(Func...) -> Visitor <Func...>;
Создаем контейнер с std::variant.
vector<ShapeVar> shapes_var;
Заполняем его
for(int i =0;i<SIZE;i++)
{
shapes_var.emplace_back(EquilateralTriangleVar { 5.6 });
shapes_var.emplace_back(SquareVar { 8.2 });
shapes_var.emplace_back(CircleVar { 3.1 });
}
Хм... Время заполнения вдвое меньше - чем время заполнения в случае "сырых" указателей, ведь дополнительная память не выделяется.
Вызываем функции объектов в контейнере с использованием std::visit, приведенного выше Visitor и определенных "по месту" лямбда-функций, которые манипулируют функциями объектов желаемым образом.
for (ShapeVar& shape: shapes_var)
{
visit(Visitor{
[](CircleVar& c) { cout << c.Area(); },
[](SquareVar& c) { c.Print(); },
[](EquilateralTriangleVar& c) { c.Area(); }
}, shape);
}
Время вызова тоже вдвое меньше - чем время вызова виртуальных функций в случае "сырых" указателей.
Итак - в производительности при использовании std::variant мы выигрываем. По читабельности кода - можно прочитать статьи по ссылкам, приведенным выше. Несвязанность типов внутри std::variant - это несомненный плюс. Поддержка кода - в случае добавления/изменения типов - это вопрос требующий осмысления.
See more "Another polymorphism", "Inheritance vs std::variant"
Комментарии
Отправить комментарий