Привет, Хабр! В последнее время много говорят о C++17, особенно с появлением в России национальной рабочей группы по стандартизации. На просторах сети без особых проблем можно найти короткие примеры использования последнего стандарта C++. Всё бы хорошо, но по настоящему обширного перехода на новые стандарты не наблюдается. Поэтому можем наблюдать картину, в которой любая библиотека, требующая минимум 14 стандарта уже считается modern постфактум.

В данной публикации разработаем небольшую библиотеку (3 функции (apply, filter, reduce) и одна как «домашнее задание» (map) :)) по удобной работе с гетерогенными контейнерами в рантайме (гетерогенность за счёт std::variant из 17 стандарта).

Из нового, помимо новых библиотечных типов, попробуем на вкус fold expressions и совсем немного structured binding

Введение


Для начала небольшое введение в тему гетерогенных контейнеров. Как известно, настоящих гетерогенных контейнеров, работающих в рантайме на c++ нет. В нашем распоряжении есть std::tuple, следы которого практически полностью исчезают в рантайме (not pay for what you don't use) и… впрочем всё. Всё остальное — лишь строительные блоки для построения собственных велосипедов библиотек.

Стоительных блоков, позволяющие сделать гетерогенный контейнер, два — это std::any и std::variant. Первый не помнит тип, поэтому его использование сильно ограничено. std::variant помнит тип и умеет матчить функторы на текущий тип с помощью std::visit (реализовано с помощью генерации таблицы методов и последующих переходов по ней). Реализация поистине магическая, а магия — единственное, что поможет сделать то, что сделать на первый взгляд невозможно :) (конечно возможно, ведь на c++ возможно всё). Внутри std::variant содержит не так много оверхеда, перенося boost версию в стандарт разработчики позаботились о производительности (относительной того, что было). Резюмируя, берём std::variant в качестве контейнера типов и базовой единицы гетерогенного контейнера.

Дисклеймер


Заранее предупреждаю о максимальной компактности кода. Не стоит бездумно его копировать, он был максимально облёгчён для быстрого понимания. Нет пространств имён, форвардинга ссылок и ещё много чего.

Так же не претендую на уникальность, наверняка есть подобные хорошие библиотеки :)

Начало


Для более лёгкого понимания и тестирования функций возьмём простой пример. Для этого проэмулируем обычную полиморфную структуру:

struct Circle
{
    void Print() { cout << "Circle. " << "Radius: " << radius << endl; }
    double Area() { return 3.14 * radius * radius; }

    double radius;
};

struct Square
{
    void Print() { cout << "Square. Side: " << side << endl; }
    double Area() { return side * side; }

    double side;
};

struct EquilateralTriangle
{
    void Print() { cout << "EquilateralTriangle. Side: " << side << endl; }
    double Area() { return (sqrt(3) / 4) * (side * side); }

    double side;
};

using Shape = variant<Circle, Square, EquilateralTriangle>;

Так же для сравнения будем держать в уме её простой полиморфный аналог:

struct Shape
{
    virtual void Print() = 0;
    virtual double Area() = 0;
    virtual ~Shape() {};
};

struct Circle : Shape
{
    Circle(double val) : radius(val) {}

    void Print() override { cout << "Circle. " << "Radius: " << radius << endl; }
    double Area() override { return 3.14 * radius * radius; }

    double radius;
};

struct Square : Shape
{
    Square(double val) : side(val) {}

    void Print() override { cout << "Square. Side: " << side << endl; }
    double Area() override { return side * side; }

    double side;
};

struct EquilateralTriangle : Shape
{
    EquilateralTriangle(double val) : side(val) {}

    void Print() override { cout << "EquilateralTriangle. Side: " << side << endl; }
    double Area() override { return (sqrt(3) / 4) * (side * side); }

    double side;
};

Создадим вектор и попытаемся стандартными средствами добиться полиморфного поведения. Проитерируемся по вектору и вызовем функцию Print.

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

vector<Shape*> shapes;
shapes.emplace_back(new Square(8.2));
shapes.emplace_back(new Circle(3.1));
shapes.emplace_back(new Square(1.8));
shapes.emplace_back(new EquilateralTriangle(10.4));
shapes.emplace_back(new Circle(5.7));
shapes.emplace_back(new Square(2.9));

Однако выглядит не очень современно. Голые вызовы new не внушают доверия. Перепишем:

vector<shared_ptr<Shape>> shapes;
shapes.emplace_back(make_shared<Square>(8.2));
shapes.emplace_back(make_shared<Circle>(3.1));
shapes.emplace_back(make_shared<Square>(1.8));
shapes.emplace_back(make_shared<EquilateralTriangle>(10.4));
shapes.emplace_back(make_shared<Circle>(5.7));
shapes.emplace_back(make_shared<Square>(2.9));

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

for (shared_ptr<Shape> shape: shapes)
{
    shape->Print();
}

// Вывод:
// Square. Side: 8.2
// Circle. Radius: 3.1
// Square. Side: 1.8
// EquilateralTriangle. Side: 10.4
// Circle. Radius: 5.7
// Square. Side: 2.9

Так же попробуем реализовать схожее поведение для гетерогенного контейнера:

vector<Shape> shapes;
shapes.emplace_back(EquilateralTriangle { 5.6 });
shapes.emplace_back(Square { 8.2 });
shapes.emplace_back(Circle { 3.1 });
shapes.emplace_back(Square { 1.8 });
shapes.emplace_back(EquilateralTriangle { 10.4 });
shapes.emplace_back(Circle { 5.7 });
shapes.emplace_back(Square { 2.9 });

Здесь уже никаких указателей. Без проблем можно работать с объектами на стеке. Так же вместо коструктора можно использовать aggregate initialization для в «меру простых» типов.

Однако просто проитерироваться и вызвать функцию уже не удастся. Попробуем сделать это средствами, которые предоставляет std::variant. Для этого имеем функцию std::visit, так же нужно создать класс функторов.

Всё будет выглядеть подобным образом:

struct Visitor
{
    void operator()(Circle& c) { c.Print(); }
    void operator()(Square& c) { c.Print(); }
    void operator()(EquilateralTriangle& c) { c.Print(); }
};

...
...
...

for (Shape& shape: shapes)
{
    visit(Visitor{}, shape);
}

Вывод аналогичный. Так же такое же поведение можно проэмулировать с помощью constexpr if. Здесь уже кому что больше нравится.

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

Реализуем самые частые и всеобъемлющие функции: apply, filter, reduce.

Шаг 1


Для начала упростим себе задачу. Первый шаг достаточно примитивен — описывался уже не раз.
Возьмём variadic templates, механизм наследования и знание о том, что лямбда-функции разворачиваются в обычные стуктуры — функторы. Hаследуемся от набора лябмд и создадим функцию, которая поможет нам вывести шаблонные типы:

template < typename... Func >
class Visitor : Func... { using Func::operator()...; }
template < class... Func > make_visitor(Func...) -> Visitor < Func... >;

Теперь вместо создания классов с функторами мы можем пользоваться набором лямбд, которые будут матчится по своим сигнатурам:

for (Shape& shape: shapes)
{
    visit(make_visitor(
                        [](Circle& c) { c.Print(); },
                        [](Square& c) { c.Print(); },
                        [](EquilateralTriangle& c) { c.Print(); }
                      ), shape);
}

Также можем воспользоваться выводом типов с generic параметром:

for (Shape& shape: shapes)
{
    visit(make_visitor([](auto& c) { c.Print(); }), shape);
}

Получилось достаточно симпатично и в меру коротко.

Apply


Осталось собрать всё вместе и получить функцию apply для гетерогенных последовательностей:

template <
           typename InputIter,
           typename InputSentinelIter,
           typename... Callable
         >
void apply(InputIter beg,
           InputSentinelIter end,
           Callable... funcs)
{
    for (auto _it = beg; _it != end; ++_it)
        visit(make_visitor(funcs...), *_it);
};

Готово. Показанная техника на новизну не претендует, любой разработчик, так или иначе работавший с boost::variant давно реализовал для себя нечто подобное http://en.cppreference.com/w/cpp/utility/variant/visit, https://habrahabr.ru/post/270689/).

Теперь мы можем использовать функцию подобным образом:

apply(shapes.begin(), shapes.end(), [](auto& shape) { shape.Print(); });

или

apply(shapes.begin(), shapes.end(), 
      [] (Circle& shape)              { shape.Print(); },
      [] (Square& shape)              { shape.Print(); },
      [] (EquilateralTriangle& shape) { shape.Print(); });

Как видите, получилось довольно непохо. Однако, если мы передадим функторы не для всех типов, которые есть в std::variant, получится ошибка компиляции. Чтобы избежать этого, по подобию SFINAE сделаем функтор с elipsis, который будет вызываться при отсутствии любой другой альтернативы, причём в порядке вызова он будет самым последним вариантом.

template <
           typename InputIter,
           typename InputSentinelIter,
           typename... Callable
         >
void apply(InputIter beg,
           InputSentinelIter end,
           Callable... funcs)
{
    for (auto _it = beg; _it != end; ++_it)
        visit(make_visitor(funcs..., [](...){}), *_it);
};

Теперь можем передавать функторы не для всех типов, для отсутствующих будет произведён вызов пустой лямбды:

// Выводит информацию только для типов Circle
apply(shapes.begin(), shapes.end(), [] (Circle& shape) { shape.Print(); }); 

Для наглядного примера просто покажу, как сделать подобное с помощью динамического полиморфизма:

// Выводит информацию только для типов Circle
for_each(shapes.begin(), shapes.end(),
         [] (shared_ptr<Shape> shape) {
             if (dynamic_pointer_cast<Circle>(shape))
                    shape->Print();
         });

Далеко не самый приятный вид.

Filter


По аналогии сделаем функцию filter. Смысловая нагрузка практически не отличается кроме того, что лямбда, имеющая elipsis в сигнатуре должна возвращать значение типа bool. Будем считать, что если мы не передали функтор, обрабатывающих какой то конкретный тип, то мы не хотим его видеть его экземпляры в отфильтрованном контейнере.

template <
           typename InputIter,
           typename InputSentinelIter,
           typename OutputIter,
           typename... Callable
         >
void filter(InputIter beg,
            InputSentinelIter end,
            OutputIter out,
            Callable... funcs)
{
    for (auto _it = beg; _it != end; ++_it)
    {
        if (visit(make_visitor(funcs..., [] (...) { return false; }), 
                  *_it))
            *out++ = *_it;
    }
};

Пользоваться реализованной функцией можно следующим образом:

vector<Shape> filtered;
filter(shapes.begin(), shapes.end(),
       back_inserter(filtered),
       [] (Circle& c) { return c.radius > 4.; },
       [] (Square& s) { return s.side < 5.; });

apply(filtered.begin(), filtered.end(), [](auto& shape) { shape.Print(); });

// Вывод:
// Square. Side: 1.8
// Circle. Radius: 5.7
// Square. Side: 2.9

Аналог, реализованный с помощью динамического полиморфизма:

vector<shared_ptr<Shape>> filtered;
copy_if(shapes.begin(), shapes.end(),
        back_inserter(filtered),
        [] (shared_ptr<Shape> shape)
        {
            if (auto circle = dynamic_pointer_cast<Circle>(shape))
            {
                return circle->radius > 4.;
            }
            else if (auto square = dynamic_pointer_cast<Square>(shape))
            {
                return square->side < 5.;
            }
            else return false;
        });

for_each(filtered.begin(), filtered.end(), [](shared_ptr<Shape> shape) { shape->Print(); });

// Вывод:
// Square. Side: 1.8
// Circle. Radius: 5.7
// Square. Side: 2.9

Reduce


Осталось реализовать reduce (аналог std::accumulate) и map (аналог std::transform). Реализация этих функций несколько сложнее, чем это было с apply и filter. Для reduce мы используем функторы с двумя параметрами (значение аккумулятора и сам объект). Для того, чтобы реализовать схожее поведение, можно частично применить лямбда функции таким образом, чтобы для std::variant остались функции одного аргумента. Красивого решения для c++ по частичному применению нет, быстрый способ — захват необходимого контекста с помощью другой лямбды. Учитывая, что мы работаем не с одной лямбдой, а с variadic pack, код раздувается и начинает быть плохо читаемым. Спасает нас обработка вариадиков с помощью fold expressions. Ветераны знают, какими костылями приходилось раньше сворачивать списки типов.

template <
           typename InputIter,
           typename InputSentinelIter,
           typename AccType,
           typename... Callable
         >
struct reduce < InputIter, InputSentinelIter, AccType, false, Callable... >
{
    constexpr auto operator()(InputIter beg, InputSentinelIter end,
                              AccType initial_acc, Callable... funcs)
    {
        for (auto _it = beg; _it != end; ++_it)
        {
            initial_acc = visit(utility::make_overloaded_from_tup(
                tup_funcs(initial_acc, funcs...),
                make_index_sequence<sizeof...(Callable)>{},
                [&initial_acc] (...) { return initial_acc; } ),
                                *_it);
        }
        return initial_acc;
    }
};

Для того, чтобы сделать что то подобное, было решено воспользоваться старым добрым кортежем (std::tuple). Обработка его элементов не слишком сложная, в любой момент можно написать свою. И так, я создаю кортеж из лямбд, который трасформируется в новый кортеж путём оборачивания каждой лямбды в другую с захватом значения аккумулятора. Благо трансформация кортежа, используя новый стандарт, пишется относительно легко:

template < typename... Types, typename Func, size_t... I >
constexpr auto tuple_transform_impl(tuple<Types...> t, Func func, index_sequence<I...>)
{
    return make_tuple(func(get<I>(t)...));
}

template < typename... Types, typename Func >
constexpr auto tuple_transform(tuple<Types...> t, Func f)
{
    return tuple_transform_impl(t, f make_index_sequence<sizeof...(Types)>{});
}

Для того, чтобы создать объемлющую лябду, мне нужно знать тип второго аргумента приходящей лямбды. С помощью helper'ов, найденных на просторах интернета, можно скастовать лямбду к структуре, имеющей оператор вызова и путём матчинга получить желаемый тип.

Выглядит это всё примерно так:

template < typename Func, typename Ret, typename _, typename A, typename... Rest >
A _sec_arg_hlpr(Ret (Func::*)(_, A, Rest...));

template < typename Func >
using second_argument = decltype(_sec_arg_hlpr(&Func::operator()));

template < typename AccType, typename... Callable >
constexpr auto tup_funcs(AccType initial_acc, Callable... funcs)
{
    return tuple_transform(tuple<Callable...>{ funcs... },
        [&initial_acc](auto func) {
            return [&initial_acc, &func] (second_argument<decltype(func)> arg) {
                return func(initial_acc, arg); };
        });
}

Всё бы хорошо, но эти чудеса не работают с generic функциями, типы входных аргументов которых мы не можем получить по определению. Поэтому, используя tag dispatching и создав простенький трейт для проверки функции мы создаём для этого случая свою реализацию.
Резюмируя, получаем для reduce следующие возможности для использования:

using ShapeCountT = tuple<size_t, size_t, size_t>;
auto result = reduce(shapes.begin(), shapes.end(),
                     ShapeCountT{},
                     [] (ShapeCountT acc, Circle& item)
                     {
                         auto [cir, sq, tr] = acc;
                         return make_tuple(++cir, sq, tr);
                     },
                     [] (ShapeCountT acc, Square& item)
                     {
                         auto [cir, sq, tr] = acc;
                         return make_tuple(cir, ++sq, tr);
                     },
                     [] (ShapeCountT acc, EquilateralTriangle& item)
                     {
                         auto [cir, sq, tr] = acc;
                         return make_tuple(cir, sq, ++tr);
                     });
auto [cir, sq, tr] = result;
cout << "Circle count: " << cir
     << "\tSquare count: " << sq
     << "\tTriangle count: " << tr << endl;

// Вывод:
// Circle count: 2 Square count: 3 Triangle count: 2

Функция map реализуется на базе похожих идей, описание её реализации и саму реализацию опущу. Для тренировки своих meta скиллов предлагаю реализовать её самим :)

Что дальше?


Немного об ошибках. Сделаем шаг в сторону и увидим подобное сообщение:

Скрин ошибки


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

Есть несколько подходов, как можно элегантно или не очень сказать об истинной природе ошибки.
В следующей раз разбавим написанное с Concepts TS из gcc-7.1.

Резюмируя, можно сказать, что подобный подход может сильно пригодиться для работы с библиотеками, которым приходилось использовать TypeErasure технику, для шаблонных классов с разной специализацией, для примитивной эмуляции полиморфизма,…

А как бы дополнили / использовали этот функционал вы? Пишите в комментариях, будет интересно почитать

Вышеприведённый код доступен тут.
Поделиться с друзьями
-->

Комментарии (59)


  1. SEOVirus
    01.07.2017 23:15
    +8

    >> Теперь выглядит лучше.
    Блин, он всё страшнее и страшнее с каждой версией :))


    1. JegernOUTT
      02.07.2017 11:48

      Самому жутко нравится. Постоянно ставлю себя на место новичков и смотрю на свой код.
      Понимаю, что ничего не понимаю, хотя и стараюсь писать в общепринятых рамках


      1. Antervis
        02.07.2017 15:32

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


        1. Shchvova
          02.07.2017 17:56

          "короче" не значит "лучше". Последнее время много приходится работать с Go. Там совсем не короче все. Зато даже бегло зная язык прям все понятно, даже не вчитываясь. С "новым" С++ такого не достичь, даже с большим опытом работы с языком. Особенно если в команду затесываются любители эксперементировать. Есть опыт когда код просто стал неподдерживаемым. И человек который его написал сам решил что переписать легче чем править и дополнять.


          1. Antervis
            02.07.2017 22:54
            +1

            Один любитель поэкспериментировать налажал значит язык плохой? Важно же соотношение: одна нетривиальная строка явно хуже одной простой и явно лучше 10 простых. Можно в качестве альтернативы с++ взять старый добрый тривиально понятный чистый си. Сколько строк кода займет написание гетерогенного контейнера на си?


            1. JegernOUTT
              02.07.2017 22:57

              Лучше не так.
              Сколько строк займет написание простой и понятной альтернативы этому коду?


              template < typename... Func > class Visitor : Func... { using Func::operator()...; }
              template < class... Func > make_visitor(Func...) -> Visitor < Func... >;


              1. Antervis
                03.07.2017 08:07

                На си? Могу бегло предположить, что несколько сотен. Надо же будет эмулировать перегрузки функций. На старых плюсах без variadic templates? Variadic macros выручат конечно, но вот аналог using Func::operator()...; придется делать через публичное наследование

                п.с. есть проползал на std::overload, как раз для этих целей.


    1. SBKarr
      02.07.2017 13:12

      С каждой версией С++ приближается к чему-то хтоническому…


      1. Antervis
        02.07.2017 13:48
        +1

        Так думают либо те, кто далек от с++, либо те, кто остановился в профессиональном росте лет 5-10 назад


        1. SBKarr
          02.07.2017 14:38
          +1

          Так говорят только те, кто не видит и не пишет ни на чём, кроме с++…


          1. Antervis
            02.07.2017 15:34

            Ну да ;) Утверждение-то всё равно верно.


          1. kkx
            02.07.2017 15:59
            -11

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


            1. SBKarr
              02.07.2017 17:53
              +1

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

              Это значит, что в новом стандарте конструкции выглядят хтонически.


            1. yarric
              02.07.2017 19:01

              Например каких языков?


            1. eugenebabichenko
              03.07.2017 11:47
              +2

              А ну, альтернативы?


              Что там в 2017, уже все пишут десктопные приложения на Go, или, может, Mozilla переехала на Servo, написанный на Rust? Есть монструозная Java, есть привязанные к своим платформам C# и Swift. Причём, первые два ещё и требуют докачивать среду выполнения и устанавливать её.


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


              1. oYASo
                04.07.2017 16:19
                -1

                Многим веб-разработчикам тоже кажется, что веб работает на java, c#, python, php и далее по списку.
                А на деле оказывается, что все БД (PostgreSQL, MySQL, SQLite, MongoDB, Redis, etc) написаны на C/C++, все популярные серверы написаны на C/C++ (nginx, apache, lighttpd, etc), все популярные браузеры написаны на C/C++ (лиса, хром, опера, etc), многие веб-движки и фреймворки написаны на C/C++ (memcached, V8, NodeJS, etc), многие частоиспользуемые нетривиальные сервисы написаны на C/C++ (инфраструктура Google, Яндекса, Facebook, etc).

                Но язык, конечно, пора хоронить, потому что сайты-визитки на нем делать неудобно. Ну-ну.


                1. yarric
                  04.07.2017 23:15
                  -1

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


          1. 0xd34df00d
            05.07.2017 23:33
            +1

            Хаскель считается?


      1. vt4a2h
        02.07.2017 16:43
        +1

        Что вы понимаете под словом «хтонический»? Никак не могу понять, что вы имеете ввиду.


        1. SBKarr
          02.07.2017 17:56
          +4

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


          1. vt4a2h
            02.07.2017 23:36

            Тогда согласен с вами :)


      1. staticlab
        02.07.2017 17:19
        -1

        До Rust ему по хтоничности всё же далеко.


        1. yarric
          02.07.2017 19:05
          +1

          Мне вообще трудно понять, для кого Rust сделан: любители знаков препинаний плотно сидят на C++, любители Python не оценят.


        1. DarkEld3r
          06.07.2017 17:20

          Вот эта строчка должна напугать? Можно было бы что-то "пострашнее" найти.


    1. yarric
      02.07.2017 19:03

      Нужно различать фичи, которые добавлены для авторов библиотек от фич для прикладных программистов. Например благодаря auto на C++ стало поудобнее писать, чем на Java/С#.


      1. RdSpclL
        05.07.2017 15:19
        +1

        Я конечно далек от С#, но разве их var не что-то похожее на auto?


  1. marsianin
    01.07.2017 23:56

    Вопрос по reduce: а нельзя было использовать std::bind вместо лямбды?


    1. JegernOUTT
      02.07.2017 11:37

      На первый взгляд — как будто можно. Но там могут возникнуть вопросы с сигнатурой сгенерированного функтора.
      В общем нужно попробовать на досуге :)


  1. Antervis
    02.07.2017 09:05
    +1

    Текст этой ошибки не удастся разобрать, даже если вы используете очень простой код

    чтобы избежать таких вот проблем, необходимо проверять входные значения через std::enable_if. А это ОЧЕНЬ много кода


    1. JegernOUTT
      02.07.2017 11:35

      Да, поэтому я и упомянул Concepts TS, который можно потрогать в последних gcc. Ну и какую — никакую реализацию можно глянуть в репозитории, ссылка на который в конце статьи


      1. Antervis
        02.07.2017 13:53

        Жаль что они не попали в с++17, там бы и ranges могли подтянуться.

        Кстати, а в каких заголовочных файлах определены library concepts?


        1. JegernOUTT
          02.07.2017 14:18

          Насколько я понимаю, пока нигде. В репозитории gcc на гитхабе концепты есть только в тестовых файлах


  1. VioletGiraffe
    02.07.2017 09:39

    vector shapes;
    shapes.emplace_back(EquilateralTriangle { 5.6 });
    shapes.emplace_back(Square { 8.2 });

    Не понял. vector — это std::vector? Это же не скомпилируется. Где тут variant?


    1. JegernOUTT
      02.07.2017 11:33
      +1

      using Shape = variant<Circle, Square, EquilateralTriangle>;

      vector<Shape> shapes;
      shapes.emplace_back(EquilateralTriangle { 5.6 });
      shapes.emplace_back(Square { 8.2 });


      1. VioletGiraffe
        02.07.2017 22:35

        А-а, слона не приметил! Благодарю)


  1. prograholic
    02.07.2017 15:16
    +1

    Вот как можно сделать НАСТОЯЩИЕ гетерогенные контейнеры: https://gieseanw.wordpress.com/2017/05/03/a-true-heterogeneous-container-in-c/


    1. JegernOUTT
      02.07.2017 15:26

      В статье, в принципе, пошли по пути создания собственного контейнера-адаптера с запоминанием складываемых типов :). От того же класса — визитора для последующей работы с ним никуда не ушли)


      Здесь же лёгкие функции для более удобной работы с std::variant, который упрятан в любой неассоциативный контейнер.


      1. prograholic
        02.07.2017 16:48

        Мой комментарий был к предложению в статье, о том что полноценные гетерогенные контейнеры в C++ сделать нельзя. Можно! Правда реализация и недостатки не позволяют использовать это в production.

        Что касается visitor, по-моему, по другому обойти гетерогенный контейнер не получится, так что в любой реализации будет visitor


        1. JegernOUTT
          02.07.2017 16:52

          Эмм,… А где написано, что нельзя?)


          1. prograholic
            02.07.2017 17:33

            Ну дык я и говорю, что можно :)

            p.s. Вы правы, ваша формулировка допускает, что они могут быть :)


    1. encyclopedist
      02.07.2017 15:29

      А в boost недавно приняли библиотеку poly_collection.


  1. Shchvova
    02.07.2017 17:49
    +1

    Пишу на С++ с 90х, и с каждой версией все больше и больше нравится простой С, а сейчас — Go. Потому что там нельзя написать что-то вроде


    template < typename... Func > class Visitor : Func... { using Func::operator()...; }
    template < class... Func > make_visitor(Func...) -> Visitor < Func... >;

    Потому что это write-only код. Его нельзя пробежать глазами и сразу понять что он делает. Даже если его сам написал месяц назад. Единственный плюс такого кода — почесать себе эго тем что красиво что-то написал и сэкономил десяток строк. Код должен быть для людей. Компилятору пофиг что компилировать ведь.


    1. Shchvova
      02.07.2017 17:52
      -1

      P.S. несмотря на мои кряхтения, статья хорошо описывает std::variant и с чем его кушать. Спасибо.


    1. JegernOUTT
      02.07.2017 18:04
      +2

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


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


    1. yarric
      02.07.2017 19:08

      Это окупается удобством использования библиотек в прикладных программах.


      Go какбы ни разу не альтернатива С++ из-за сборщика мусора и вот этого всего.


  1. izvolov
    02.07.2017 21:59
    +1

    Как известно, настоящих гетерогенных контейнеров, работающих в рантайме на c++ нет...

    Я как раз разбирал один способ создания неоднородных контейнеров.
    Ну и выше тоже давали ссылку.


  1. AlexPublic
    03.07.2017 04:16
    +1

    В начале мелкое замечание не по сути статьи: заменой «устаревшего» new в данном случае очевидно должна служить функция make_unique, а не make_shared, как в вашем тексте. Хорошо ещё что в данной статье нет сравнительного теста производительности, иначе данная мелкая оплошность могла бы квалифицироваться уже как подделка под нужные результаты.

    Далее, по самой статье. Вы как-то быстро перешли к мелким техническим деталям, совершенно не пояснив зачем это вообще нужно. Т.е. из вашей статьи совсем не очевидно когда надо применять полиморфизм на виртуальных функциях, а когда указанный подход. Лично я начал бы подобную статью с перечисления возможных вариантов реализации подобных динамических гетерогенных коллекций (например виртуальные функции, variant, any) и обсуждениях их плюсов и минусов. Типа такого:
    — виртуальные функции: реализуются через ссылочное размещение (минус — ухудшение производительности из-за лишней косвенности), требуют правки хранимых типов данных в виде добавления общего наследника (минус — нельзя сделать коллекцию из int, double, string), не ограничивают число хранимых типов (плюс — можно бесконечно расширяться за счёт новых классов-потомков).
    — variant: реализуются через размещение по месту (плюс — нет ухудшение производительности), не требуют правки хранимых типов данных (плюс — можно сделать коллекцию из int, double, string), ограничивают число хранимых типов (минус — нельзя добавить новые хранимые типы без модификации кода контейнера).
    — any: реализуются через ссылочное размещение (минус — ухудшение производительности из-за лишней косвенности), не требуют правки хранимых типов данных (плюс — можно сделать коллекцию из int, double, string), не ограничивают число хранимых типов (плюс — можно расширяться на любые хранимые типы).

    А т.к. в вашей статье в качестве примера приводится как раз случай реализуемый с помощью виртуальных функций (в контейнере нет например базовых типов) и нет тестов производительности, то из неё совершенно непонятно в чём вообще смысл применения подобных решений. Единственная попытка сравнения свелась к весьма субъективному понятию красоты кода, причём в данном конкретном случае ещё и не вполне однозначному (для данного конкретного случая вариант на полиморфных функциях будет покрасивее, если записать всё корректно).

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


    1. JegernOUTT
      03.07.2017 10:09

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


      1. AlexPublic
        03.07.2017 11:31

        Ну как бы variant и any — это совсем не игрушки, а вполне фундаментальные вещи, введённые в стандарт. Собственно это C++ аналоги union и void* из языка C. И важно понимать преимущества и недостатки их применения.

        Более того, если мы взглянем на библиотечку TypeErasure из Boost'а (т.е. опять же очевидно, что уже не игрушка, а вполне используемое на практике), то увидим, что в ней центральную роль занимает слегка расширенная версия any, позволяющая реализовать все указанные вами в статье примеры приблизительно в одну строчку. )))


        1. JegernOUTT
          03.07.2017 11:35

          variant и any — не игрушки)
          А то, что написал я — пока игрушки:)
          По крайней мере до тех пор, пока не подойду к вопросу более фундаментально)


  1. nikitablack
    03.07.2017 10:57

    Объясните пожалуйста что означает этот код:


    template < typename... Func >
    class Visitor : Func... { using Func::operator()...; }
    template < class... Func > make_visitor(Func...) -> Visitor < Func... >;

    После объявления класса не стоит ;, поэтому это не скомпилируется. Почему, при объявлении параметров шаблона в одном месте использован typename, а в другом — class? И самое главное — что означает последняя строка? Выглядит как User-defined deduction guides, но не компилируется из-за неправильного синтаксиса.


    1. Antervis
      03.07.2017 11:40
      +2

      автор просто сказал «реализуйте что-то вроде этого:». Настоящий код (упрощенная версия, без perfect forwarding'а и корректного noexceptness) выглядел бы так:

      template <typename ...Func>
      struct Visitor : Func... {
          Visitor(Func ...f) : Func(f)... {}
          using Func::operator()...;
      };
      
      template <typename ...Func>
      auto make_visitor(Func...f) {
          return Visitor<Func...>(f...);
      }
      


      Пример использования. Тут функторы должны быть copy-coonstructible


  1. babylon
    03.07.2017 11:54
    +1

    Как гетерогенные контейнеры выглядят на ассемблере?


    1. MikailBag
      03.07.2017 20:49

      А в языке с отсутствующей типизацией есть деление на гомо- и гетерогенные?


  1. VitaminPSG
    03.07.2017 12:47
    +1

    Спасибо за статью, очень интересно. P.s. В примерах у вас опечатка "[]](Circle& c) { c.Print(); ", лишняя ] скобочка.


    1. JegernOUTT
      03.07.2017 12:50

      Спасибо, поправил)


    1. attuda
      04.07.2017 00:50

      о! а я, читая вчера статью с телефона, думаю: не забыть потом загуглить, что за новый вид захвата переменных в лямбды придумали, и почему такой вычурный синтаксис?)


  1. graphican
    04.07.2017 13:28
    -1

    Если код понятен менее 90% среднестатистических программистов — нафиг такой код. Код пишет один программист, но поддерживать его приходится годами другим программистам.


  1. babylon
    05.07.2017 16:01
    -2

    Я выбираю языки по уровню технологичности. С++17 весьма неплох, но от языка жду полного вытеснения классов.и замены их контейнерами.