В данной публикации разработаем небольшую библиотеку (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++
Дисклеймер
Заранее предупреждаю о максимальной компактности кода. Не стоит бездумно его копировать, он был максимально облёгчён для быстрого понимания. Нет пространств имён, форвардинга ссылок и ещё много чего.
Так же не претендую на уникальность, наверняка есть подобные хорошие библиотеки :)
Начало
Для более лёгкого понимания и тестирования функций возьмём простой пример. Для этого проэмулируем обычную полиморфную структуру:
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)
marsianin
01.07.2017 23:56Вопрос по reduce: а нельзя было использовать std::bind вместо лямбды?
JegernOUTT
02.07.2017 11:37На первый взгляд — как будто можно. Но там могут возникнуть вопросы с сигнатурой сгенерированного функтора.
В общем нужно попробовать на досуге :)
Antervis
02.07.2017 09:05+1Текст этой ошибки не удастся разобрать, даже если вы используете очень простой код
чтобы избежать таких вот проблем, необходимо проверять входные значения через std::enable_if. А это ОЧЕНЬ много кодаJegernOUTT
02.07.2017 11:35Да, поэтому я и упомянул Concepts TS, который можно потрогать в последних gcc. Ну и какую — никакую реализацию можно глянуть в репозитории, ссылка на который в конце статьи
Antervis
02.07.2017 13:53Жаль что они не попали в с++17, там бы и ranges могли подтянуться.
Кстати, а в каких заголовочных файлах определены library concepts?JegernOUTT
02.07.2017 14:18Насколько я понимаю, пока нигде. В репозитории gcc на гитхабе концепты есть только в тестовых файлах
VioletGiraffe
02.07.2017 09:39vector shapes;
shapes.emplace_back(EquilateralTriangle { 5.6 });
shapes.emplace_back(Square { 8.2 });
Не понял. vector — это std::vector? Это же не скомпилируется. Где тут variant?JegernOUTT
02.07.2017 11:33+1using Shape = variant<Circle, Square, EquilateralTriangle>;
vector<Shape> shapes; shapes.emplace_back(EquilateralTriangle { 5.6 }); shapes.emplace_back(Square { 8.2 });
prograholic
02.07.2017 15:16+1Вот как можно сделать НАСТОЯЩИЕ гетерогенные контейнеры: https://gieseanw.wordpress.com/2017/05/03/a-true-heterogeneous-container-in-c/
JegernOUTT
02.07.2017 15:26В статье, в принципе, пошли по пути создания собственного контейнера-адаптера с запоминанием складываемых типов :). От того же класса — визитора для последующей работы с ним никуда не ушли)
Здесь же лёгкие функции для более удобной работы с std::variant, который упрятан в любой неассоциативный контейнер.
prograholic
02.07.2017 16:48Мой комментарий был к предложению в статье, о том что полноценные гетерогенные контейнеры в C++ сделать нельзя. Можно! Правда реализация и недостатки не позволяют использовать это в production.
Что касается visitor, по-моему, по другому обойти гетерогенный контейнер не получится, так что в любой реализации будет visitorJegernOUTT
02.07.2017 16:52Эмм,… А где написано, что нельзя?)
prograholic
02.07.2017 17:33Ну дык я и говорю, что можно :)
p.s. Вы правы, ваша формулировка допускает, что они могут быть :)
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 код. Его нельзя пробежать глазами и сразу понять что он делает. Даже если его сам написал месяц назад. Единственный плюс такого кода — почесать себе эго тем что красиво что-то написал и сэкономил десяток строк. Код должен быть для людей. Компилятору пофиг что компилировать ведь.
Shchvova
02.07.2017 17:52-1P.S. несмотря на мои кряхтения, статья хорошо описывает
std::variant
и с чем его кушать. Спасибо.
JegernOUTT
02.07.2017 18:04+2У меня были такие же мысли, когда было мало практики с variadic packs. Нужно побольше попрактиковаться, понять концепции, которые двигали авторов при создании такого синтаксиса и всё становится намного легче, в частности и с вышеприведенным кодом :)
Но не могу не согласиться, что всё это далеко от совершенства, особенно когда начинаешь навешивать форвардинг ссылок, sfinae защиту и т.п. В особо
страшныхсложных случаях получившуюся кашу даже страшно потом открывать
yarric
02.07.2017 19:08Это окупается удобством использования библиотек в прикладных программах.
Go какбы ни разу не альтернатива С++ из-за сборщика мусора и вот этого всего.
izvolov
02.07.2017 21:59+1Как известно, настоящих гетерогенных контейнеров, работающих в рантайме на c++ нет...
Я как раз разбирал один способ создания неоднородных контейнеров.
Ну и выше тоже давали ссылку.
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), не ограничивают число хранимых типов (плюс — можно расширяться на любые хранимые типы).
А т.к. в вашей статье в качестве примера приводится как раз случай реализуемый с помощью виртуальных функций (в контейнере нет например базовых типов) и нет тестов производительности, то из неё совершенно непонятно в чём вообще смысл применения подобных решений. Единственная попытка сравнения свелась к весьма субъективному понятию красоты кода, причём в данном конкретном случае ещё и не вполне однозначному (для данного конкретного случая вариант на полиморфных функциях будет покрасивее, если записать всё корректно).
Ну а в остальном статья конечно же очень хорошая и чувствуется, что автор хорошо знаком с особенностями метапрограммирования на шаблонах.JegernOUTT
03.07.2017 10:09Огромное спасибо за подробный комментарий :)
По началу хотел сделать так, как вы описали. Но что то мне подсказывает, что это всё не более, чем игрушка)
Поэтому чтобы выкладывать какие то более правильные и фундаментальные сравнения, нужно ещё самому это всё поизучать на практике)
Отсюда и хаб "Ненормальное программирование", чтобы не было претензий о том, что это неприменимо на продакшене)AlexPublic
03.07.2017 11:31Ну как бы variant и any — это совсем не игрушки, а вполне фундаментальные вещи, введённые в стандарт. Собственно это C++ аналоги union и void* из языка C. И важно понимать преимущества и недостатки их применения.
Более того, если мы взглянем на библиотечку TypeErasure из Boost'а (т.е. опять же очевидно, что уже не игрушка, а вполне используемое на практике), то увидим, что в ней центральную роль занимает слегка расширенная версия any, позволяющая реализовать все указанные вами в статье примеры приблизительно в одну строчку. )))JegernOUTT
03.07.2017 11:35variant и any — не игрушки)
А то, что написал я — пока игрушки:)
По крайней мере до тех пор, пока не подойду к вопросу более фундаментально)
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, но не компилируется из-за неправильного синтаксиса.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
VitaminPSG
03.07.2017 12:47+1Спасибо за статью, очень интересно. P.s. В примерах у вас опечатка "[]](Circle& c) { c.Print(); ", лишняя ] скобочка.
attuda
04.07.2017 00:50о! а я, читая вчера статью с телефона, думаю: не забыть потом загуглить, что за новый вид захвата переменных в лямбды придумали, и почему такой вычурный синтаксис?)
graphican
04.07.2017 13:28-1Если код понятен менее 90% среднестатистических программистов — нафиг такой код. Код пишет один программист, но поддерживать его приходится годами другим программистам.
babylon
05.07.2017 16:01-2Я выбираю языки по уровню технологичности. С++17 весьма неплох, но от языка жду полного вытеснения классов.и замены их контейнерами.
SEOVirus
>> Теперь выглядит лучше.
Блин, он всё страшнее и страшнее с каждой версией :))
JegernOUTT
Самому жутко нравится. Постоянно ставлю себя на место новичков и смотрю на свой код.
Понимаю, что ничего не понимаю, хотя и стараюсь писать в общепринятых рамках
Antervis
вспоминаю себя новичком: сложнее всего мне было понимать виртуальные функции и битовую магию (по сей день её недолюбливаю). Вывод: новичку что новый код, что старый сложно читать. Просто новый в среднем короче раза в полтора-два (из личного опыта).
Shchvova
"короче" не значит "лучше". Последнее время много приходится работать с Go. Там совсем не короче все. Зато даже бегло зная язык прям все понятно, даже не вчитываясь. С "новым" С++ такого не достичь, даже с большим опытом работы с языком. Особенно если в команду затесываются любители эксперементировать. Есть опыт когда код просто стал неподдерживаемым. И человек который его написал сам решил что переписать легче чем править и дополнять.
Antervis
Один любитель поэкспериментировать налажал значит язык плохой? Важно же соотношение: одна нетривиальная строка явно хуже одной простой и явно лучше 10 простых. Можно в качестве альтернативы с++ взять старый добрый тривиально понятный чистый си. Сколько строк кода займет написание гетерогенного контейнера на си?
JegernOUTT
Лучше не так.
Сколько строк займет написание простой и понятной альтернативы этому коду?
Antervis
На си? Могу бегло предположить, что несколько сотен. Надо же будет эмулировать перегрузки функций. На старых плюсах без variadic templates? Variadic macros выручат конечно, но вот аналог using Func::operator()...; придется делать через публичное наследование
п.с. есть проползал на std::overload, как раз для этих целей.
SBKarr
С каждой версией С++ приближается к чему-то хтоническому…
Antervis
Так думают либо те, кто далек от с++, либо те, кто остановился в профессиональном росте лет 5-10 назад
SBKarr
Так говорят только те, кто не видит и не пишет ни на чём, кроме с++…
Antervis
Ну да ;) Утверждение-то всё равно верно.
kkx
+1… С++ мертв. Есть куча языков на порядок проще, элегантее и главное на которых пишут чтобы получить результат, а не обсуждать конструкции…
SBKarr
То, что я говорю о хтоническом виде конструкций в новом стандарте не значит, что мне не нравится язык, или я считаю его мёртвым. или я далёк от с++, или я остановился в профессиональном росте, или не собираюсь в этом разбираться.
Это значит, что в новом стандарте конструкции выглядят хтонически.
yarric
Например каких языков?
eugenebabichenko
А ну, альтернативы?
Что там в 2017, уже все пишут десктопные приложения на Go, или, может, Mozilla переехала на Servo, написанный на Rust? Есть монструозная Java, есть привязанные к своим платформам C# и Swift. Причём, первые два ещё и требуют докачивать среду выполнения и устанавливать её.
Все эти языки хороши, если играться ими на уютном сервере, а в случае с приложениями юзер выберет то, что легче и проще в установке.
oYASo
Многим веб-разработчикам тоже кажется, что веб работает на 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).
Но язык, конечно, пора хоронить, потому что сайты-визитки на нем делать неудобно. Ну-ну.
yarric
А ещё забавно то, что языки, начинавшие как "C++ для
тупыхобычных людей", в конечном счёте сами переизобрели тот же C++ и теперь выглядят ненамного проще.0xd34df00d
Хаскель считается?
vt4a2h
Что вы понимаете под словом «хтонический»? Никак не могу понять, что вы имеете ввиду.
SBKarr
Магически, не очевидно, страшно, запутанно, и при этом мощно, функционально, с богатыми возможностями для применения. Проще говоря, есть в этом что-то от первобытной магии.
vt4a2h
Тогда согласен с вами :)
staticlab
До Rust ему по хтоничности всё же далеко.
yarric
Мне вообще трудно понять, для кого Rust сделан: любители знаков препинаний плотно сидят на C++, любители Python не оценят.
DarkEld3r
Вот эта строчка должна напугать? Можно было бы что-то "пострашнее" найти.
yarric
Нужно различать фичи, которые добавлены для авторов библиотек от фич для прикладных программистов. Например благодаря
auto
на C++ стало поудобнее писать, чем на Java/С#.RdSpclL
Я конечно далек от С#, но разве их var не что-то похожее на auto?