Карта сайта Kansoftware
НОВОСТИУСЛУГИРЕШЕНИЯКОНТАКТЫ
Разработка программного обеспечения

Boost.Hana: User Manual

Boost , ,

Boost.Hana  1.0.1
Your standard library for metaprogramming
Описание
  • Предпосылки и установка
  • Поддержка
  • Введение
  • Быстрый старт
  • Утверждения
  • Номера времени компиляции
  • Вычисления типов
  • Интроспекция
  • Общие положения о контейнерах
  • Обобщения алгоритмов
  • Соображения производительности
  • Интеграция с внешними библиотеками
  • Hana's core
  • Организация-руководитель
  • Заключение
  • Использование Ссылка
  • Признания
  • Глоссарий
  • Рационалы/FAQ
  • Добавление I: Advanced constexpr
  • Приложение Минимальный MPL
  • Description


    Hana - это библиотека для метапрограммирования на C++, предназначенная для вычислений как по типам, так и по значениям. Функциональность, которую он предоставляет, представляет собой набор того, что обеспечивается хорошо зарекомендовавшими себя библиотеками Boost.MPL и Boost.Fusion. Используя методы реализации C++11/14 и идиомы, Hana может похвастаться более быстрым временем компиляции и производительностью исполнения на уровне или лучше, чем предыдущие библиотеки метапрограммирования, при этом заметно повышая уровень выразительности в процессе. Hana легко расширяется в специальной манере и обеспечивает взаимодействие с Boost. Слияние, рост. MPL и стандартная библиотека.

    Prerequisites and installation


    Hana - это библиотека только для заголовков без внешних зависимостей (даже от остальной части Boost). Поэтому использовать хану в собственном проекте очень просто. В принципе, просто добавьте каталог include/ в путь поиска заголовка компилятора, и вы закончите. Тем не менее, если вы хотите установить Hana на свою систему, у вас есть несколько вариантов. Во-первых, вы можете установить Boost 1.61.0 или более поздней версии, так как Хана включена в Boost начиная с этого выпуска. Если вы не хотите устанавливать все Boost, можно установить только Hana. Для этого можно скачать код из официального GitHub repository и установить библиотеку вручную, выдав из корня проекта следующие команды (требуется CMake):

    1 mkdir build && cd build
    2 cmake ..
    3 cmake --build . --target install

    Это позволит установить Hana в каталог установки по умолчанию для вашей платформы (/usr/local для Unix, C:/Program Files для Windows). Если вы хотите установить Hana в специальном месте, вы можете использовать

    1 cmake .. -DCMAKE_INSTALL_PREFIX=/custom/install/prefix
    Note
    • В ручной установке также будет установлен файлhana.pcдля использования сpkg-config.
    • Не устанавливайте Hana, как показано выше, если у вас уже есть установка Boost, потому что новая установка перезапишет ту, которая поставляется с Boost.

    Если вы используете CMake в проекте, вы можете использовать предоставленный модуль FindHana.cmake для настройки Hana в качестве внешнего проекта CMake. Модуль также позволяет установить Hana локально в этот проект, без необходимости установки Hana в систему в соответствии с вышеупомянутыми инструкциями. Наконец, если вы хотите внести свой вклад в Хану, вы можете увидеть, как лучше всего настроить среду для развития в README.

    Compiler requirements

    Библиотека опирается на компилятор C++14 и стандартную библиотеку, но больше ничего не требуется. Вот таблица текущих компиляторов/инструментов C++14 с комментариями относительно поддержки Hana:

    Compiler/Toolchain Status
    Clang >= 3.5.0Полностью работает; тестируется на каждом толчке GitHub
    Xcode > 6.3Полностью работает; тестируется на каждом толчке GitHub
    GCC >= 6.0.0Полностью работает; тестируется на каждом толчке GitHub

    Более конкретно, Hana требует компилятор / стандартную библиотеку, поддерживающую следующие функции C++14:

    • Общие лямбда
    • обобщенныйconstexpr
    • Переменные шаблоны
    • Автоматически выведенный тип возврата
    • Все черты типа C++14 из заголовка<type_traits>

    Дополнительная информация для конкретных платформ доступна на wiki.

    Support


    Если у вас есть проблема, пожалуйста, просмотрите FAQ и wiki. Поиск проблем также является хорошей идеей. Если это не поможет, не стесняйтесь общаться с нами в Gitter или открыть новую проблему. StackOverflow с тегом boost-hana является предпочтительным местом для вопросов об использовании. Если вы столкнулись с тем, что вы считаете ошибкой, пожалуйста, откройте проблему.

    Introduction


    Когда поднимется. Впервые появился MPL, который предоставил программистам C++ огромное облегчение, абстрагируя тонны шаблонного взлома за рабочим интерфейсом. Этот прорыв в значительной степени способствовал тому, что шаблонное метапрограммирование C++ стало более распространенным, и сегодня эта дисциплина глубоко укоренилась во многих серьезных проектах. В последнее время C++11 и C++14 внесли много серьезных изменений в язык, некоторые из которых значительно облегчают метапрограммирование, в то время как другие резко расширяют пространство для проектирования библиотек. Возникает закономерный вопрос: абстракции для метапрограммирования все-таки желательно иметь, а если да, то какие? После изучения различных вариантов, таких как MPL11, ответ в конечном итоге пришел сам по себе в виде библиотеки. Ключевое понимание Ханы заключается в том, что манипулирование типами и ценностями — это всего лишь две стороны одной медали. Объединяя обе концепции, метапрограммирование становится проще, и перед нами открываются новые захватывающие возможности.

    C++ computational quadrants

    Но чтобы действительно понять, что такое Хана, важно понимать различные типы вычислений на C++. Мы сосредоточим наше внимание на четырех различных видах вычислений, хотя было бы возможно более тонкое разделение зерна. Во-первых, у нас есть вычисления во время выполнения, которые являются обычными вычислениями, которые мы используем в C++. В этом мире у нас есть контейнеры времени выполнения, функции времени выполнения и алгоритмы времени выполнения:

    auto f = [](int i) -> std::string {
    return std::to_string(i * i);
    };
    std::vector<int> ints{1, 2, 3, 4};
    std::vector<std::string> strings;
    std::transform(ints.begin(), ints.end(), std::back_inserter(strings), f);
    assert((strings == std::vector<std::string>{"1", "4", "9", "16"}));

    Обычным набором инструментов для программирования в этом квадранте является стандартная библиотека C++, которая предоставляет многоразовые алгоритмы и контейнеры, работающие во время выполнения. С C++11 возможен второй вид вычислений: constexpr. Там у нас есть контейнеры constexpr, функции constexpr и алгоритмы constexpr:

    constexpr int factorial(int n) {
    return n == 0 ? 1 : n * factorial(n - 1);
    }
    template <typename T, std::size_t N, typename F>
    // ...
    }
    constexpr std::array<int, 4> ints{{1, 2, 3, 4}};
    constexpr std::array<int, 4> facts = transform(ints, factorial);
    static_assert(facts == std::array<int, 4>{{1, 2, 6, 24}}, "");
    Note
    For the above code to actually work, std::array's operator== would have to be marked constexpr, which is not the case (even in C++14).

    В принципе, вычисление constexpr отличается от вычисления времени выполнения тем, что оно достаточно просто для оценки (по сути, интерпретации) компилятором. В общем, любая функция, которая не выполняет ничего слишком недружественного к оценщику компилятора (например, бросание или выделение памяти), может быть помечена constexpr без каких-либо дальнейших изменений. Это делает вычисления constexpr очень похожими на вычисления во время выполнения, за исключением того, что вычисления constexpr более ограничены, и они получают возможность оцениваться во время компиляции. К сожалению, нет широко используемого набора инструментов для constexpr-программирования, т.е. нет широко принятой «стандартной библиотеки» для constexpr программирования. Однако библиотека Sprout может быть полезной для тех, кто интересуется вычислениями constexpr.

    Третий вид вычислений — гетерогенные вычисления. Гетерогенные вычисления отличаются от обычных вычислений тем, что вместо контейнеров, содержащих однородные объекты (все объекты, имеющие один и тот же тип), контейнеры могут содержать объекты с разными типами. Кроме того, функции в этом квадранте вычислений являются гетерогенными функциями, что является сложным способом говорить о шаблонных функциях. Аналогично, у нас есть гетерогенные алгоритмы, которые манипулируют гетерогенными контейнерами и функциями:

    auto to_string = [](auto t) {
    std::stringstream ss;
    ss << t;
    return ss.str();
    };
    fusion::vector<int, std::string, float> seq{1, "abc", 3.4f};
    fusion::vector<std::string, std::string, std::string>
    strings = fusion::transform(seq, to_string);
    assert(strings == fusion::make_vector("1"s, "abc"s, "3.4"s));

    Если манипулирование гетерогенными контейнерами кажется вам слишком странным, просто подумайте об этом как о прославленной манипуляции std::tuple. В мире C++03 библиотека для выполнения такого рода вычислений представляет собой Boost.Fusion, которая предоставляет несколько структур данных и алгоритмов для управления гетерогенными коллекциями данных. Четвертый и последний квадрант вычислений, который мы рассмотрим здесь, — это квадрант вычислений на уровне типов. В этом квадранте есть контейнеры уровня типа, функции уровня типа (обычно называемые метафункциями) и алгоритмы уровня типа. Здесь все работает на типах: контейнеры удерживают типы, а метафункции принимают типы в качестве аргументов и возвращают типы в качестве результатов.

    template <typename T>
    struct add_const_pointer {
    using type = T const*;
    };
    using types = mpl::vector<int, char, float, void>;
    using pointers = mpl::transform<types, add_const_pointer<mpl::_1>>::type;
    static_assert(mpl::equal<
    pointers,
    mpl::vector<int const*, char const*, float const*, void const*>
    >::value, "");

    Сфера вычислений на уровне типов была изучена достаточно широко, и де-факто решение для вычислений на уровне типов в C++03 представляет собой библиотеку под названием Boost.MPL, которая предоставляет контейнеры и алгоритмы на уровне типов. Для низкоуровневых преобразований типов метафункции, предоставляемые стандартным заголовком , также могут использоваться с C++11.

    What is this library about?

    Все хорошо, но что это за библиотека? Теперь, когда мы установили таблицу, прояснив типы вычислений, доступных нам на C++, ответ может показаться вам очень простым. Целью Ханы является объединение 3-го и 4-го квадрантов вычислений. Более конкретно, Хана является (долгосроченным) конструктивным доказательством того, что гетерогенные вычисления строго более мощны, чем вычисления уровня типа, и что поэтому мы можем выразить любые вычисления уровня типа эквивалентными гетерогенными вычислениями. Это строительство осуществляется в два этапа. Сначала Hana - это полнофункциональная библиотека разнородных алгоритмов и контейнеров, немного похожая на модернизированный Boost. Слияние. Во-вторых, Hana предоставляет способ перевода любых вычислений уровня типа в эквивалентные гетерогенные вычисления и обратно, что позволяет повторно использовать весь механизм гетерогенных вычислений для вычислений уровня типа без какого-либо дублирования кода. Конечно, самое большое преимущество этой унификации видит пользователь, как вы сами увидите.

    Quick start


    Цель этого раздела - представить основные концепции библиотеки с очень высокого уровня и в довольно быстром темпе; не волнуйтесь, если вы не понимаете всего, что вот-вот будет брошено вам. Тем не менее, это руководство предполагает, что читатель уже по крайней мере знаком с базовым метапрограммированием и стандартом C++14 . Во-первых, включим библиотеку:

    #include <boost/hana.hpp>
    namespace hana = boost::hana;

    Если не указано иное, документация предполагает, что вышеуказанные строки должны присутствовать перед примерами и фрагментами кода. Также обратите внимание на то, что более мелкие заголовки представлены и будут объяснены в разделе Организация руководителя . Для быстрого запуска давайте теперь включим некоторые дополнительные заголовки и определим некоторые прекрасные типы животных, которые нам понадобятся ниже:

    #include <cassert>
    #include <iostream>
    #include <string>
    struct Fish { std::string name; };
    struct Cat { std::string name; };
    struct Dog { std::string name; };

    Если вы читаете эту документацию, скорее всего, вы уже знаете std::tuple и std::make_tuple. Hana предоставляет собственный кортеж и make_tuple:

    auto animals = hana::make_tuple(Fish{"Nemo"}, Cat{"Garfield"}, Dog{"Snoopy"});

    Это создает кортеж, который похож на массив, за исключением того, что он может содержать элементы с различными типами. Контейнеры, которые могут содержать элементы с различными типами, называются гетерогенными контейнерами. В то время как стандартная библиотека предоставляет очень мало операций для манипулирования std::tuples, Hana предоставляет несколько операций и алгоритмов для манипулирования своими собственными кортежами:

    using namespace hana::literals;
    // Access tuple elements with operator[] instead of std::get.
    Cat garfield = animals[1_c];
    // Perform high level algorithms on tuples (this is like std::transform)
    auto names = hana::transform(animals, [](auto a) {
    return a.name;
    });
    assert(hana::reverse(names) == hana::make_tuple("Snoopy", "Garfield", "Nemo"));
    Note
    1_c is a C++14 user-defined literal creating a compile-time number. These user-defined literals are contained in the boost::hana::literals namespace, hence the using directive.

    Обратите внимание, как мы передаем C++14 дженерик лямбда трансформировать ; это необходимо, потому что лямбда сначала будет называться Рыба , затем Кошка и, наконец, Собака , которые все имеют разные типы. Хана предоставляет большинство алгоритмов, предоставляемых стандартной библиотекой C++, за исключением того, что они работают на кортежах и связанных с ними гетерогенных контейнерах вместо std::vector & friends. В дополнение к работе с гетерогенными значениями, Hana позволяет выполнять вычисления на уровне типов с естественным синтаксисом, все во время компиляции и без каких-либо накладных расходов. Это компилирует и делает только то, что вы ожидаете:

    auto animal_types = hana::make_tuple(hana::type_c<Fish*>, hana::type_c<Cat&>, hana::type_c<Dog>);
    auto no_pointers = hana::remove_if(animal_types, [](auto a) {
    return hana::traits::is_pointer(a);
    });
    static_assert(no_pointers == hana::make_tuple(hana::type_c<Cat&>, hana::type_c<Dog>), "");
    Note
    type_c<...> is not a type! It is a C++14 variable template yielding an object representing a type for Hana. This is explained in the section on type computations.

    В дополнение к гетерогенным и компилируемым временным последовательностям, Хана предоставляет несколько функций, чтобы сделать ваши кошмары метапрограммирования в прошлом. Например, можно проверить наличие элемента структуры с одной простой строкой вместо того, чтобы полагаться на неуклюжие взломы SFINAE:

    auto has_name = hana::is_valid([](auto&& x) -> decltype((void)x.name) { });
    static_assert(has_name(garfield), "");
    static_assert(!has_name(1), "");

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

    // 1. Give introspection capabilities to 'Person'
    struct Person {
    BOOST_HANA_DEFINE_STRUCT(Person,
    (std::string, name),
    (int, age)
    );
    };
    // 2. Write a generic serializer (bear with std::ostream for the example)
    auto serialize = [](std::ostream& os, auto const& object) {
    hana::for_each(hana::members(object), [&](auto member) {
    os << member << std::endl;
    });
    };
    // 3. Use it
    Person john{"John", 30};
    serialize(std::cout, john);
    // output:
    // John
    // 30

    Это круто, но я уже слышу, как ты жалуешься на непонятные сообщения об ошибках. Однако оказывается, что Хана была создана для людей, а не для профессиональных шаблонных метапрограммистов, и это показывает. Давай нарочно облажаемся и посмотрим, какой беспорядок на нас накинулся. Во-первых, ошибка:

    auto serialize = [](std::ostream& os, auto const& object) {
    hana::for_each(os, [&](auto member) {
    // ^^ oopsie daisy!
    os << member << std::endl;
    });
    };

    Итак, наказание:

    error: static_assert failed "hana::for_each(xs, f) requires 'xs' to be Foldable"
    static_assert(Foldable<S>::value,
    ^ ~~~~~~~~~~~~~~~~~~
    note: in instantiation of function template specialization
    'boost::hana::for_each_t::operator()<
    std::__1::basic_ostream<char> &, (lambda at [snip])>' requested here
    hana::for_each(os, [&](auto member) {
    ^
    note: in instantiation of function template specialization
    'main()::(anonymous class)::operator()<Person>' requested here
    serialize(std::cout, john);
    ^

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

    A real world example

    В этом разделе наша цель будет заключаться в реализации своего рода заявления switch, способного обрабатывать boost::anys. При наличии boost::any цель состоит в отправке функции, связанной с динамическим типом any:

    boost::any a = 'x';
    std::string r = switch_(a)(
    case_<int>([](auto i) { return "int: "s + std::to_string(i); }),
    case_<char>([](auto c) { return "char: "s + std::string{c}; }),
    default_([] { return "unknown"s; })
    );
    assert(r == "char: x"s);
    Note
    In the documentation, we will often use the s suffix on string literals to create std::strings without syntactic overhead. This is a standard-defined C++14 user-defined literal.

    Поскольку любой из них содержит char, вторая функция называется с char внутри него. Если бы any содержал int, первая функция была бы названа с int внутри него. Когда динамический тип any не соответствует ни одному из рассмотренных случаев, вместо этого называется функция default_. Наконец, результат switch является результатом вызова функции, связанной с динамическим типом any. Тип этого результата определяется как общий тип результата всех предоставленных функций:

    boost::any a = 'x';
    auto r = switch_(a)(
    case_<int>([](auto) -> int { return 1; }),
    case_<char>([](auto) -> long { return 2l; }),
    default_([]() -> long long { return 3ll; })
    );
    // r is inferred to be a long long
    static_assert(std::is_same<decltype(r), long long>{}, "");
    assert(r == 2ll);

    Теперь мы рассмотрим, как эта утилита может быть реализована с помощью Hana. Первый шаг — связать каждый тип с функцией. Для этого мы представляем каждый case_ как hana::pair Первый элемент — это тип, а второй — функция. Кроме того, мы (произвольно) решаем представить случай default_ как hana::pair, отображая тип манекена в функцию:

    template <typename T>
    auto case_ = [](auto f) {
    return hana::make_pair(hana::type_c<T>, f);
    };
    struct default_t;
    auto default_ = case_<default_t>;

    Чтобы обеспечить интерфейс, который мы показали выше, switch_ должен будет вернуть функцию, принимающую кейсы. Другими словами, switch_(a) должна быть функцией, принимающей любое количество случаев (которые являются hana::pairs), и выполняющей логику для отправки a в правую функцию. Этого можно легко достичь, имея switch_ возвращает C++14 общий лямбда:

    template <typename Any>
    auto switch_(Any& a) {
    return [&a](auto ...cases_) {
    // ...
    };
    }

    Однако, поскольку наборы параметров не очень гибкие, мы поместим чехлы в кортеж, чтобы мы могли ими манипулировать:

    template <typename Any>
    auto switch_(Any& a) {
    return [&a](auto ...cases_) {
    auto cases = hana::make_tuple(cases_...);
    // ...
    };
    }

    Обратите внимание, как auto Ключевое слово используется при определении cases; часто проще позволить компилятору вывести тип кортежа и использовать make_tuple вместо ручной разработки типов. Следующим шагом является отделение дела по умолчанию от остальных дел. Именно здесь вещи начинают становиться интересными. Для этого мы используем алгоритм Ханы find_if, который работает немного как std::find_if:

    template <typename Any>
    auto switch_(Any& a) {
    return [&a](auto ...cases_) {
    auto cases = hana::make_tuple(cases_...);
    auto default_ = hana::find_if(cases, [](auto const& c) {
    return hana::first(c) == hana::type_c<default_t>;
    });
    // ...
    };
    }

    find_if берет tuple и предикат, и возвращает первый элемент кортежа, который удовлетворяет предикату. Результат возвращается в виде hana::optional, который очень похож на std::optional, за исключением того, является ли это необязательное значение пустым или не известно во время компиляции. Если предикат не удовлетворён для какого-либо элемента корзины, find_if возвращает ничего (пустое значение). В противном случае он возвращает just(x) (непустое значение), где x является первым элементом, удовлетворяющим предикату. В отличие от предикатов, используемых в алгоритмах STL, используемый здесь предикат должен быть общим, потому что элементы кортежа неоднородны. Кроме того, этот предикат должен возвращать то, что Хана называет IntegralConstant, что означает, что результат предиката должен быть известен во время компиляции. Эти детали объясняются в разделе кросс-фазные алгоритмы. Внутри предиката мы просто сравниваем тип первого элемента корпуса с type_c. Если вы помните, что мы использовали hana::pairs для кодирования случаев, это просто означает, что мы находим случай по умолчанию среди всех предоставленных случаев. Но что, если не было представлено ни одного случая дефолта? Конечно, мы должны потерпеть неудачу во время компиляции!

    template <typename Any>
    auto switch_(Any& a) {
    return [&a](auto ...cases_) {
    auto cases = hana::make_tuple(cases_...);
    auto default_ = hana::find_if(cases, [](auto const& c) {
    return hana::first(c) == hana::type_c<default_t>;
    });
    static_assert(default_ != hana::nothing,
    "switch is missing a default_ case");
    // ...
    };
    }

    Обратите внимание, как мы можем использовать static_assert в результате сравнения с nothing, даже если default_ не является объектом constexpr? Смело, Хана гарантирует, что никакая информация, известная во время компиляции, не будет потеряна во время выполнения, что явно относится к присутствию случая default_. Следующим шагом является сбор набора дел, не связанных с дефолтом. Для этого мы используем алгоритм filter, который сохраняет только элементы последовательности, удовлетворяющие предикату:

    template <typename Any>
    auto switch_(Any& a) {
    return [&a](auto ...cases_) {
    auto cases = hana::make_tuple(cases_...);
    auto default_ = hana::find_if(cases, [](auto const& c) {
    return hana::first(c) == hana::type_c<default_t>;
    });
    static_assert(default_ != hana::nothing,
    "switch is missing a default_ case");
    auto rest = hana::filter(cases, [](auto const& c) {
    return hana::first(c) != hana::type_c<default_t>;
    });
    // ...
    };
    }

    Следующим шагом является поиск первого случая, соответствующего динамическому типу any, а затем вызов функции, связанной с этим случаем. Самый простой способ сделать это — использовать классическую рекурсию с вариадными пакетами параметров. Конечно, мы могли бы переплетать алгоритмы Ханы запутанным способом, чтобы достичь этого, но иногда лучший способ сделать что-то - это написать это с нуля, используя основные методы. Для этого мы вызовем функцию реализации с содержимым кортежа rest, используя функцию unpack:

    template <typename Any>
    auto switch_(Any& a) {
    return [&a](auto ...cases_) {
    auto cases = hana::make_tuple(cases_...);
    auto default_ = hana::find_if(cases, [](auto const& c) {
    return hana::first(c) == hana::type_c<default_t>;
    });
    static_assert(default_ != hana::nothing,
    "switch is missing a default_ case");
    auto rest = hana::filter(cases, [](auto const& c) {
    return hana::first(c) != hana::type_c<default_t>;
    });
    return hana::unpack(rest, [&](auto& ...rest) {
    return process(a, a.type(), hana::second(*default_), rest...);
    });
    };
    }

    unpack принимает tuple и функцию и вызывает функцию с содержанием tuple в качестве аргументов. Результат unpack является результатом вызова этой функции. В нашем случае функция представляет собой общую лямбду, которая, в свою очередь, вызывает функцию процесса . Наша причина использования unpack заключалась в том, чтобы превратить rest в набор параметров аргументов, которые легче обрабатывать рекурсивно, чем кортежи. Прежде чем перейти к функции process, стоит объяснить, что такое second(*default_). Как мы объясняли ранее, default_ является необязательным значением. Как и std::optional, это дополнительное значение перегружает оператор отсчета (и оператор стрелки), чтобы разрешить доступ к значению внутри optional. Если опция пуста ( ничего), запускается ошибка времени компиляции. Поскольку мы знаем, что default_ не пуст (мы проверили это чуть выше), мы просто передаем функцию, связанную с случаем по умолчанию, функции process. Теперь мы готовы к последнему шагу, который заключается в реализации функции процесса :

    template <typename Any, typename Default>
    auto process(Any&, std::type_index const&, Default& default_) {
    return default_();
    }
    template <typename Any, typename Default, typename Case, typename ...Rest>
    auto process(Any& a, std::type_index const& t, Default& default_,
    Case& case_, Rest& ...rest)
    {
    using T = typename decltype(+hana::first(case_))::type;
    return t == typeid(T) ? hana::second(case_)(*boost::unsafe_any_cast<T>(&a))
    : process(a, t, default_, rest...);
    }

    Существует две перегрузки этой функции: перегрузка, когда есть по крайней мере один случай для обработки, и перегрузка базового случая, когда есть только случай по умолчанию. Как и следовало ожидать, базовый случай просто вызывает функцию по умолчанию и возвращает этот результат. Другая перегрузка немного интереснее. Во-первых, мы извлекаем тип, связанный с этим случаем, и сохраняем его в T. Этот деклапт(...)::тип танец может показаться запутанным, но на самом деле это довольно просто. Грубо говоря, это принимает тип, представленный как объект (a type) и оттягивает его обратно к уровню типа (a T). Подробности описаны в разделе вычисления на уровне типов . Затем мы сравниваем, соответствует ли динамический тип any этому случаю, и если да, то мы называем функцию, связанную с этим случаем, функцией any, отлитой под соответствующий тип. В противном случае мы просто называем процесс рекурсивно с остальными случаями. Довольно просто, не так ли? Вот окончательное решение:

    #include <boost/hana.hpp>
    #include <boost/any.hpp>
    #include <cassert>
    #include <string>
    #include <typeindex>
    #include <typeinfo>
    #include <utility>
    namespace hana = boost::hana;
    //! [cases]
    template <typename T>
    auto case_ = [](auto f) {
    return hana::make_pair(hana::type_c<T>, f);
    };
    struct default_t;
    auto default_ = case_<default_t>;
    //! [cases]
    //! [process]
    template <typename Any, typename Default>
    auto process(Any&, std::type_index const&, Default& default_) {
    return default_();
    }
    template <typename Any, typename Default, typename Case, typename ...Rest>
    auto process(Any& a, std::type_index const& t, Default& default_,
    Case& case_, Rest& ...rest)
    {
    using T = typename decltype(+hana::first(case_))::type;
    return t == typeid(T) ? hana::second(case_)(*boost::unsafe_any_cast<T>(&a))
    : process(a, t, default_, rest...);
    }
    //! [process]
    //! [switch_]
    template <typename Any>
    auto switch_(Any& a) {
    return [&a](auto ...cases_) {
    auto cases = hana::make_tuple(cases_...);
    auto default_ = hana::find_if(cases, [](auto const& c) {
    return hana::first(c) == hana::type_c<default_t>;
    });
    static_assert(default_ != hana::nothing,
    "switch is missing a default_ case");
    auto rest = hana::filter(cases, [](auto const& c) {
    return hana::first(c) != hana::type_c<default_t>;
    });
    return hana::unpack(rest, [&](auto& ...rest) {
    return process(a, a.type(), hana::second(*default_), rest...);
    });
    };
    }
    //! [switch_]

    Это для быстрого старта! В этом примере представлено только несколько полезных алгоритмов (find_if, filter, unpack) и гетерогенных контейнеров (tuple, optional), но будьте уверены, что их гораздо больше. В следующих разделах учебника постепенно вводятся общие понятия, относящиеся к Хане, но вы можете использовать следующий чит-лист для быстрой ссылки, если хотите сразу начать кодирование. Этот чит-лист содержит наиболее часто используемые алгоритмы и контейнеры, а также краткое описание того, что делает каждый из них.

    Cheatsheet

    Remarks

    • Большинство алгоритмов работают как над типами, так и над значениями (см. раздел о вычислениях типа).
    • Алгоритмы всегда возвращают свой результат в виде нового контейнера; мутация на месте никогда не выполняется (см. раздел об алгоритмах).
    • Все алгоритмы являются объектами функцийconstexpr.
    container description
    tuple General purpose index-based heterogeneous sequence with a fixed length. Use this as a std::vector for heterogeneous objects.
    optional Represents an optional value, i.e. a value that can be empty. This is a bit like std::optional, except that the emptiness is known at compile-time.
    map Unordered associative array mapping (unique) compile-time entities to arbitrary objects. This is like std::unordered_map for heterogeneous objects.
    set Unordered container holding unique keys that must be compile-time entities. This is like std::unordered_set for heterogeneous objects.
    range Represents an interval of compile-time numbers. This is like std::integer_sequence, but better.
    pair Container holding two heterogeneous objects. Like std::pair, but compresses the storage of empty types.
    string Compile-time string.
    type Container representing a C++ type. This is the root of the unification between types and values, and is of interest for MPL-style computations (type-level computations).
    integral_constant Represents a compile-time number. This is very similar to std::integral_constant, except that hana::integral_constant also defines operators and more syntactic sugar.
    lazy Encapsulates a lazy value or computation.
    basic_tuple Stripped-down version of hana::tuple. Not standards conforming, but more compile-time efficient.
    function description
    adjust(sequence, value, f) Apply a function to each element of a sequence that compares equal to some value and return the result.
    adjust_if(sequence, predicate, f) Apply a function to each element of a sequence satisfying some predicate and return the result.
    {all,any,none}(sequence) Returns whether all/any/none of the elements of a sequence are true-valued.
    {all,any,none}_of(sequence, predicate) Returns whether all/any/none of the elements of the sequence satisfy some predicate.
    append(sequence, value) Append an element to a sequence.
    at(sequence, index) Returns the n-th element of a sequence. The index must be an IntegralConstant.
    back(sequence) Returns the last element of a non-empty sequence.
    concat(sequence1, sequence2) Concatenate two sequences.
    contains(sequence, value) Returns whether a sequence contains the given object.
    count(sequence, value) Returns the number of elements that compare equal to the given value.
    count_if(sequence, predicate) Returns the number of elements that satisfy the predicate.
    drop_front(sequence[, n]) Drop the first n elements from a sequence, or the whole sequence if length(sequence) <= n. n must be an IntegralConstant. When not provided, n defaults to 1.
    drop_front_exactly(sequence[, n]) Drop the first n elements from a sequence. n must be an IntegralConstant and the sequence must have at least n elements. When not provided, n defaults to 1.
    drop_back(sequence[, n]) Drop the last n elements from a sequence, or the whole sequence if length(sequence) <= n. n must be an IntegralConstant. When not provided, n defaults to 1.
    drop_while(sequence, predicate) Drops elements from a sequence while a predicate is satisfied. The predicate must return an IntegralConstant.
    fill(sequence, value) Replace all the elements of a sequence with some value.
    filter(sequence, predicate) Remove all the elements that do not satisfy a predicate. The predicate must return an IntegralConstant.
    find(sequence, value) Find the first element of a sequence which compares equal to some value and return just it, or return nothing. See hana::optional.
    find_if(sequence, predicate) Find the first element of a sequence satisfying the predicate and return just it, or return nothing. See hana::optional.
    flatten(sequence) Flatten a sequence of sequences, a bit like std::tuple_cat.
    fold_left(sequence[, state], f) Accumulates the elements of a sequence from the left, optionally with a provided initial state.
    fold_right(sequence[, state], f) Accumulates the elements of a sequence from the right, optionally with a provided initial state.
    fold(sequence[, state], f) Equivalent to fold_left; provided for consistency with Boost.MPL and Boost.Fusion.
    for_each(sequence, f) Call a function on each element of a sequence. Returns void.
    front(sequence) Returns the first element of a non-empty sequence.
    group(sequence[, predicate]) Group adjacent elements of a sequence which all satisfy (or all do not satisfy) some predicate. The predicate defaults to equality, in which case the elements must be Comparable.
    insert(sequence, index, element) Insert an element at a given index. The index must be an IntegralConstant.
    insert_range(sequence, index, elements) Insert a sequence of elements at a given index. The index must be an IntegralConstant.
    is_empty(sequence) Returns whether a sequence is empty as an IntegralConstant.
    length(sequence) Returns the length of a sequence as an IntegralConstant.
    lexicographical_compare(sequence1, sequence2[, predicate]) Performs a lexicographical comparison of two sequences, optionally with a custom predicate, by default with hana::less.
    maximum(sequence[, predicate]) Returns the greatest element of a sequence, optionally according to a predicate. The elements must be Orderable if no predicate is provided.
    minimum(sequence[, predicate]) Returns the smallest element of a sequence, optionally according to a predicate. The elements must be Orderable if no predicate is provided.
    partition(sequence, predicate) Partition a sequence into a pair of elements that satisfy some predicate, and elements that do not satisfy it.
    prepend(sequence, value) Prepend an element to a sequence.
    remove(sequence, value) Remove all the elements that are equal to a given value.
    remove_at(sequence, index) Remove the element at the given index. The index must be an IntegralConstant.
    remove_if(sequence, predicate) Remove all the elements that satisfy a predicate. The predicate must return an IntegralConstant.
    remove_range(sequence, from, to) Remove the elements at indices in the given [from, to) half-open interval. The indices must be IntegralConstants.
    replace(sequence, oldval, newval) Replace the elements of a sequence that compare equal to some value by some other value.
    replace_if(sequence, predicate, newval) Replace the elements of a sequence that satisfy some predicate by some value.
    reverse(sequence) Reverse the order of the elements in a sequence.
    reverse_fold(sequence[, state], f) Equivalent to fold_right; provided for consistency with Boost.MPL and Boost.Fusion.
    size(sequence) Equivalent to length; provided for consistency with the C++ standard library.
    slice(sequence, indices) Returns a new sequence containing the elements at the given indices of the original sequence.
    slice_c<from, to>(sequence) Returns a new sequence containing the elements at indices contained in [from, to) of the original sequence.
    sort(sequence[, predicate]) Sort (stably) the elements of a sequence, optionally according to a predicate. The elements must be Orderable if no predicate is provided.
    take_back(sequence, number) Take the last n elements of a sequence, or the whole sequence if length(sequence) <= n. n must be an IntegralConstant.
    take_front(sequence, number) Take the first n elements of a sequence, or the whole sequence if length(sequence) <= n. n must be an IntegralConstant.
    take_while(sequence, predicate) Take elements of a sequence while some predicate is satisfied, and return that.
    transform(sequence, f) Apply a function to each element of a sequence and return the result.
    unique(sequence[, predicate]) Removes all consecutive duplicates from a sequence. The predicate defaults to equality, in which case the elements must be Comparable.
    unpack(sequence, f) Calls a function with the contents of a sequence. Equivalent to f(x1, ..., xN).
    zip(s1, ..., sN) Zip N sequences into a sequence of tuples. All the sequences must have the same length.
    zip_shortest(s1, ..., sN) Zip N sequences into a sequence of tuples. The resulting sequence has the length of the shortest input sequence.
    zip_with(f, s1, ..., sN) Zip N sequences with a N-ary function. All the sequences must have the same length.
    zip_shortest_with(f, s1, ..., sN) Zip N sequences with a N-ary function. The resulting sequence has the length of the shortest input sequence.

    Assertions


    В остальной части этого руководства вы найдете фрагменты кода, где используются различные виды утверждений, такие как Boost_HANA_RUNTIME_CHECK и Boost_HANA_CONSTANT_CHECK. Как и любой разумный макрос assert, они в основном проверяют, что данное им условие выполнено. Однако в контексте гетерогенного программирования некоторые сведения известны во время компиляции, в то время как другие известны только во время выполнения. Точный тип утверждения, которое используется в контексте, говорит вам, может ли условие, которое утверждается, быть известно во время компиляции или должно быть вычислено во время выполнения, что является очень ценной информацией. Вот различные виды утверждений, используемых в учебнике, с небольшим описанием их особенностей. Для получения более подробной информации вы должны проверить ссылку на утверждения .

    assertion description
    BOOST_HANA_RUNTIME_CHECK Assertion on a condition that is not known until runtime. This assertion provides the weakest form of guarantee.
    BOOST_HANA_CONSTEXPR_CHECK Assertion on a condition that would be constexpr if lambdas were allowed inside constant expressions. In other words, the only reason for it not being a static_assert is the language limitation that lambdas can't appear in constant expressions, which might be lifted in C++17.
    static_assert Assertion on a constexpr condition. This is stronger than BOOST_HANA_CONSTEXPR_CHECK in that it requires the condition to be a constant expression, and it hence assures that the algorithms used in the expression are constexpr-friendly.
    BOOST_HANA_CONSTANT_CHECK Assertion on a boolean IntegralConstant. This assertion provides the strongest form of guarantee, because an IntegralConstant can be converted to a constexpr value even if it is not constexpr itself.

    Compile-time numbers


    В этом разделе представлено важное понятие IntegralConstant и философия парадигмы метапрограммирования Ханы. Начнем с довольно странного вопроса. Что такое integral_constant?

    template<class T, T v>
    struct integral_constant {
    static constexpr T value = v;
    typedef T value_type;
    typedef integral_constant type;
    constexpr operator value_type() const noexcept { return value; }
    constexpr value_type operator()() const noexcept { return value; }
    };
    Note
    If this is totally new to you, you might want to take a look at the documentation for std::integral_constant.

    Один верный ответ заключается в том, что integral_constant представляет собой кодирование числа на уровне типа или, в более общем смысле, любого объекта интегрального типа. Для иллюстрации мы могли бы очень легко определить функцию преемника на числах в этом представлении, используя псевдоним шаблона:

    template <typename N>
    using succ = integral_constant<int, N::value + 1>;
    using one = integral_constant<int, 1>;
    using two = succ<one>;
    using three = succ<two>;
    // ...

    Это способ integral_constants обычно рассматривается как type-level объекты, которые могут быть использованы для шаблонного метапрограммирования. Другой способ увидеть integral_constant — это объект времени выполнения, представляющий constexpr. Значение интегрального типа:

    auto one = integral_constant<int, 1>{};

    Здесь, хотя one не помечается как constexpr, абстрактное значение, которое он содержит (a constexpr 1), все еще доступно во время компиляции, поскольку это значение кодируется в типе one. Действительно, даже если one не является constexpr, мы можем использовать decltype для извлечения значения времени компиляции, которое он представляет:

    auto one = integral_constant<int, 1>{};
    constexpr int one_constexpr = decltype(one)::value;

    Но почему мы должны рассматривать integral_constant как объекты, а не объекты на уровне типов? Чтобы понять, почему, рассмотрим, как мы могли бы теперь реализовать ту же функцию преемника, что и раньше:

    template <typename N>
    auto succ(N) {
    return integral_constant<int, N::value + 1>{};
    }
    auto one = integral_constant<int, 1>{};
    auto two = succ(one);
    auto three = succ(two);
    // ...

    Вы заметили что-то новое? Разница в том, что вместо реализации succ на уровне типов с псевдонимом шаблона мы теперь реализуем его на уровне значений с функцией шаблона. Кроме того, теперь мы можем выполнять арифметику времени компиляции, используя тот же синтаксис, что и обычный C++. Этот способ рассматривать объекты времени компиляции вместо типов является ключом к выразительной силе Ханы.

    Compile-time arithmetic

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

    template <typename X, typename Y>
    struct plus {
    using type = integral_constant<
    decltype(X::value + Y::value),
    >;
    };
    using three = plus<integral_constant<int, 1>,
    integral_constant<int, 2>>::type;

    Рассматривая integral_constants как объекты вместо типов, перевод от метафункции к функции очень прост:

    template <typename V, V v, typename U, U u>
    constexpr auto
    operator+(integral_constant<V, v>, integral_constant<U, u>)
    { return integral_constant<decltype(v + u), v + u>{}; }
    auto three = integral_constant<int, 1>{} + integral_constant<int, 2>{};

    Очень важно подчеркнуть тот факт, что этот оператор не возвращает нормальное целое число. Вместо этого он возвращает объект с инициализацией значения, тип которого содержит результат добавления. Единственная полезная информация, содержащаяся в этом объекте, на самом деле в его типе, и мы создаем объект, потому что он позволяет нам использовать этот приятный синтаксис уровня ценности. Получается, что мы можем сделать этот синтаксис еще лучше, используя шаблон переменных C++14, чтобы упростить создание integral_constant:

    Теперь мы говорим о заметном увеличении выразительности по сравнению с первоначальным подходом на уровне типов, не так ли? Но есть и другие; мы также можем использовать C++14 пользовательские определенные буквы , чтобы сделать этот процесс еще проще:

    template <char ...digits>
    constexpr auto operator"" _c() {
    // parse the digits and return an integral_constant
    }
    auto three = 1_c + 3_c;

    Hana предоставляет свои собственные integral_constant, которые определяют арифметические операторы так же, как мы показали выше. Hana также предоставляет переменные шаблоны для легкого создания различных типов integral_constants: int_c, long_c, bool_c и т.д. Это позволяет опустить брекеты {}, необходимые для инициализации этих объектов. Конечно, суффикс _c также предусмотрен; он является частью пространства имен hana::literals, и вы должны импортировать его в свое пространство имен, прежде чем использовать его:

    using namespace hana::literals;
    auto three = 1_c + 3_c;

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

    Example: Euclidean distance

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

    \[ \mathrm{distance}\left((x_1, y_1), (x_2, y_2)\right) := \sqrt{(x_1 - x_2)^2 + (y_1 - y_2)^2} \]

    Во-первых, вот как это выглядит при подходе на уровне типа (с использованием MPL):

    template <typename P1, typename P2>
    struct distance {
    using xs = typename mpl::minus<typename P1::x,
    typename P2::x>::type;
    using ys = typename mpl::minus<typename P1::y,
    typename P2::y>::type;
    using type = typename sqrt<
    typename mpl::plus<
    typename mpl::multiplies<xs, xs>::type,
    typename mpl::multiplies<ys, ys>::type
    >::type
    >::type;
    };
    static_assert(mpl::equal_to<
    distance<point<mpl::int_<3>, mpl::int_<5>>,
    point<mpl::int_<7>, mpl::int_<2>>>::type,
    mpl::int_<5>
    >::value, "");

    Да. Теперь давайте реализуем его с помощью подхода, представленного выше:

    template <typename P1, typename P2>
    constexpr auto distance(P1 p1, P2 p2) {
    auto xs = p1.x - p2.x;
    auto ys = p1.y - p2.y;
    return sqrt(xs*xs + ys*ys);
    }
    BOOST_HANA_CONSTANT_CHECK(distance(point(3_c, 5_c), point(7_c, 2_c)) == 5_c);

    Эта версия выглядит более чистой. Однако это еще не все. Обратите внимание, как функция distance выглядит точно так же, как вы бы написали для вычисления евклидового расстояния по динамическим значениям? Действительно, поскольку мы используем один и тот же синтаксис для динамической и компиляционной арифметики, общие функции, написанные для одного, будут работать для обоих!

    auto p1 = point(3, 5); // dynamic values now
    auto p2 = point(7, 2); //
    BOOST_HANA_RUNTIME_CHECK(distance(p1, p2) == 5); // same function works!

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

    Compile-time branching

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

    template <typename T, typename ...Args>
    std::enable_if_t<std::is_constructible<T, Args...>::value,
    std::unique_ptr<T>> make_unique(Args&&... args) {
    return std::unique_ptr<T>(new T(std::forward<Args>(args)...));
    }
    template <typename T, typename ...Args>
    std::enable_if_t<!std::is_constructible<T, Args...>::value,
    std::unique_ptr<T>> make_unique(Args&&... args) {
    return std::unique_ptr<T>(new T{std::forward<Args>(args)...});
    }

    Этот код создает std::unique_ptr, используя правильную форму синтаксиса для конструктора. Для этого он использует SFINAE и требует двух разных перегрузок. Теперь любой здравомыслящий, впервые увидев это, спросит, почему нельзя просто написать:

    template <typename T, typename ...Args>
    std::unique_ptr<T> make_unique(Args&&... args) {
    return std::unique_ptr<T>(new T(std::forward<Args>(args)...));
    else
    return std::unique_ptr<T>(new T{std::forward<Args>(args)...});
    }

    Причина в том, что компилятор должен компилировать обе ветви заявления if, независимо от состояния (даже если оно известно во время компиляции). Но когда T является not сконструируемым из Args..., вторая ветвь не будет компилироваться, что вызовет тяжелую ошибку компиляции. Нам нужен способ сказать компилятору не компилировать вторую ветвь, когда условие истинно, и первую ветвь, когда условие ложно.

    Чтобы эмулировать это, Хана предоставляет функцию if_, которая работает немного как нормальное утверждение if, за исключением того, что она принимает условие, которое может быть IntegralConstant и возвращает одно из двух значений (которые могут иметь разные типы), выбранных условием. Если условие истинно, первое значение возвращается, а в противном случае возвращается второе значение. Несколько тщетным примером является следующее:

    auto one_two_three = hana::if_(hana::true_c, 123, "hello");
    auto hello = hana::if_(hana::false_c, 123, "hello");
    Note
    hana::true_c and hana::false_c are just boolean IntegralConstants representing a compile-time true value and a compile-time false value, respectively.

    Здесь one_two_three равно 123, а hello равно "hello". Другими словами, , если_ немного похож на троичного условного оператора :, за исключением того, что обе стороны : могут иметь разные типы:

    // fails in both cases because both branches have incompatible types
    auto one_two_three = hana::true_c ? 123 : "hello";
    auto hello = hana::false_c ? 123 : "hello";

    Хорошо, это аккуратно, но как это может помочь нам написать полные ветви, которые лениво инстанцируются компилятором? Ответ состоит в том, чтобы представить обе ветви утверждения if, которое мы хотели бы написать в виде общих лямбда, и использовать hana::if_, чтобы вернуть ветвь, которую мы хотели бы выполнить. Вот как можно переписать make_unique:

    template <typename T, typename ...Args>
    std::unique_ptr<T> make_unique(Args&&... args) {
    return hana::if_(std::is_constructible<T, Args...>{},
    [](auto&& ...x) { return std::unique_ptr<T>(new T(std::forward<Args>(x)...)); },
    [](auto&& ...x) { return std::unique_ptr<T>(new T{std::forward<Args>(x)...}); }
    )(std::forward<Args>(args)...);
    }

    Здесь первое значение, данное hana:: если_, представляет собой общую лямбду, представляющую ветвь, которую мы хотим выполнить, если условие истинно, а второе значение - ветвь, которую мы хотим выполнить иначе. hana::if_ просто возвращает ветвь, выбранную условием, и мы называем эту ветвь (которая является общей лямбдой) сразу с std::forward(args).... Следовательно, правильная родовая лямбда в конечном итоге вызывается, причем x... является args..., и мы возвращаем результат этого вызова.

    Причина, по которой этот подход работает, заключается в том, что тело каждой ветви может быть инстанциировано только тогда, когда известны типы всех x.... Действительно, поскольку ветвь является родовой лямбдой, тип аргумента не известен до тех пор, пока лямбда не будет названа, и компилятор должен ждать, пока типы x... будут известны до проверки типа тела лямбды. Поскольку ошибочная лямбда никогда не называется, когда условие не удовлетворено (hana:: если_ позаботится об этом), тело лямбды, которое потерпит неудачу, никогда не проверяется по типу и не происходит ошибки компиляции.

    Note
    The branches inside the if_ are lambdas. As such, they really are different functions from the make_unique function. The variables appearing inside those branches have to be either captured by the lambdas or passed to them as arguments, and so they are affected by the way they are captured or passed (by value, by reference, etc..).

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

    template <typename T, typename ...Args>
    std::unique_ptr<T> make_unique(Args&&... args) {
    return hana::if_(std::is_constructible<T, Args...>{},
    [&] { return std::unique_ptr<T>(new T(std::forward<Args>(args)...)); },
    [&] { return std::unique_ptr<T>(new T{std::forward<Args>(args)...}); }
    );
    }

    Здесь мы фиксируем переменные args... из области охвата, что не позволяет нам вводить новые переменные x... и передавать их в качестве аргументов в ветви. Однако у этого есть две проблемы. Во-первых, это не приведет к правильному результату, поскольку hana::if_ в конечном итоге вернет лямбду вместо того, чтобы вернуть результат вызова этой лямбды. Для этого можно использовать hana::eval_if вместо hana::if_:

    template <typename T, typename ...Args>
    std::unique_ptr<T> make_unique(Args&&... args) {
    return hana::eval_if(std::is_constructible<T, Args...>{},
    [&] { return std::unique_ptr<T>(new T(std::forward<Args>(args)...)); },
    [&] { return std::unique_ptr<T>(new T{std::forward<Args>(args)...}); }
    );
    }

    Здесь мы фиксируем прилагаемые args... по ссылке с использованием [&], и нам не нужно получать никаких аргументов. Кроме того, hana::eval_if предполагает, что его аргументы являются ветвями, которые можно назвать, и он позаботится о вызове ветви, которая выбрана условием. Однако это все равно приведет к провалу компиляции, потому что тела лямбда больше не зависят, и семантический анализ будет проводиться для обеих ветвей, хотя в конечном итоге будет использоваться только одна. Решение этой проблемы состоит в том, чтобы сделать тела лямбда искусственно зависимыми от чего-то, чтобы компилятор не мог выполнять семантический анализ до того, как лямбда будет фактически использована. Чтобы сделать это возможным, hana::eval_if вызовет выбранную ветвь с функцией идентичности (функцией, которая возвращает свой аргумент без изменений), если ветвь принимает такой аргумент:

    template <typename T, typename ...Args>
    std::unique_ptr<T> make_unique(Args&&... args) {
    return hana::eval_if(std::is_constructible<T, Args...>{},
    [&](auto _) { return std::unique_ptr<T>(new T(std::forward<Args>(_(args))...)); },
    [&](auto _) { return std::unique_ptr<T>(new T{std::forward<Args>(_(args))...}); }
    );
    }

    Здесь тела ветвей принимают дополнительный аргумент, называемый _. Этот аргумент будет предоставлен hana::eval_if выбранной ветви. Затем мы используем _ в качестве функции на переменных, которые мы хотим сделать зависимыми в теле каждой ветви. Происходит то, что _ всегда будет функцией, которая возвращает свой аргумент без изменений. Однако компилятор не может знать его до того, как лямбда была фактически названа, поэтому он не может знать тип _(args). Это не позволяет компилятору выполнять семантический анализ, и ошибки компиляции не происходит. Кроме того, поскольку _(x) гарантированно эквивалентен x, мы знаем, что на самом деле не изменяем семантику ветвей, используя этот трюк.

    Хотя использование этого трюка может показаться громоздким, оно может быть очень полезным при работе со многими переменными внутри ветви. Кроме того, не требуется обертывать все переменные _; должны быть обернуты только переменные, которые участвуют в выражении, проверка типа которого должна быть отложена. Есть еще несколько вещей, которые нужно знать о ветвлении времени компиляции в Хане, но вы можете копнуть глубже, посмотрев на ссылку для hana::eval_if, hana::if_ и hana::lazy.

    Why stop here?

    Почему мы должны ограничиваться арифметическими операциями и ветвлением? Когда вы начинаете рассматривать IntegralConstants как объекты, становится разумным увеличить их интерфейс с большим количеством функций, которые обычно полезны. Например, функция Hana IntegralConstant определяет функцию члена times, которая может использоваться для вызова функции определенное количество раз, что особенно полезно для прокрутки петли:

    __attribute__((noinline)) void f() { }
    int main() {
    hana::int_c<10>.times(f);
    }

    В приведенном выше коде 10 вызовов f расширяются во время компиляции. Это эквивалентно написанию

    f(); f(); ... f(); // 10 times
    Note
    Always be careful about manually unrolling loops or doing other such optimizations manually. In most cases, your compiler is probably better than you at optimizing. When in doubt, benchmark.

    Еще одно приятное использование IntegralConstants заключается в определении привлекательных операторов для индексации гетерогенных последовательностей. Принимая во внимание, что std::tuple должен быть доступен с std::get, hana::tuple может быть доступен с помощью знакомого оператора[], используемого для стандартных библиотечных контейнеров:

    auto values = hana::make_tuple(1, 'x', 3.4f);
    char x = values[1_c];

    Как это работает, очень просто. В основном, hana::tuple определяет оператор [], принимающий IntegralConstant вместо нормального целого числа, аналогично

    template <typename N>
    constexpr decltype(auto) operator[](N const&) {
    return std::get<N::value>(*this);
    }

    Это конец раздела IntegralConstants. В этом разделе представлено чувство, стоящее за новым способом метапрограммирования Ханы; если вам понравилось то, что вы видели до сих пор, остальная часть этого учебника должна чувствовать себя как дома.

    Type computations


    На данный момент, если вы заинтересованы в выполнении вычислений на уровне типов, как в MPL, вам может быть интересно, как Хана поможет вам. Не отчаивайтесь. Хана предоставляет способ выполнять вычисления на уровне типов с большой выразительностью, представляя типы как значения, так же, как мы представляли числа времени компиляции как значения. Это совершенно новый способ подхода к метапрограммированию, и вы должны попытаться немного отбросить свои старые привычки, если вы хотите стать опытным с Ханой.

    Тем не менее, имейте в виду, что современные функции C++, такие как автоматически выведенный тип возврата , во многих случаях устраняют необходимость в вычислениях типа. Следовательно, прежде чем даже рассматривать возможность вычисления типа, вы должны спросить себя, есть ли более простой способ достичь того, чего вы пытаетесь достичь. В большинстве случаев ответ будет «да». Однако, когда ответ будет отрицательным, Хана предоставит вам ядерные мощности, чтобы сделать то, что должно быть сделано.

    Types as objects

    Ключом к подходу Ханы к вычислениям на уровне типов по существу является тот же подход к арифметике времени компиляции. По сути, идея состоит в том, чтобы представить объекты времени компиляции, завернув их в какой-то контейнер. Для IntegralConstant объекты времени компиляции были постоянными выражениями интегрального типа, а обертка, которую мы использовали, была integral_constant. В этом разделе объекты времени компиляции будут типами, а обертка, которую мы будем использовать, называется type. Так же, как мы сделали для IntegralConstants, давайте начнем с определения шаблона, который можно использовать для представления типа:

    template <typename T>
    struct basic_type {
    // empty (for now)
    };
    basic_type<int> Int{};
    basic_type<char> Char{};
    // ...
    Note
    We're using the name basic_type here because we're only building a naive version of the actual functionality provided by Hana.

    Хотя это может показаться совершенно бесполезным, на самом деле достаточно начать писать метафункции, которые выглядят как функции. Действительно, рассмотрим следующие альтернативные реализации std::add_pointer и std::is_pointer:

    template <typename T>
    constexpr basic_type<T*> add_pointer(basic_type<T> const&)
    { return {}; }
    template <typename T>
    constexpr auto is_pointer(basic_type<T> const&)
    { return hana::bool_c<false>; }
    template <typename T>
    constexpr auto is_pointer(basic_type<T*> const&)
    { return hana::bool_c<true>; }

    Мы только что написали метафункции, которые выглядят как функции, так же, как мы написали арифметические метафункции компилятивного времени в качестве гетерогенных операторов C++ в предыдущем разделе. Вот как мы можем их использовать:

    basic_type<int> t{};
    auto p = add_pointer(t);

    Обратите внимание, как теперь мы можем использовать синтаксис вызова нормальной функции для выполнения вычислений на уровне типов? Это аналогично тому, как использование значений для чисел времени компиляции позволило нам использовать обычные операторы C++ для выполнения вычислений времени компиляции. Как и для integral_constant, мы также можем сделать еще один шаг вперед и использовать шаблоны переменных C++14 для обеспечения синтаксического сахара для создания типов:

    template <typename T>
    constexpr basic_type<T> type_c{};
    auto t = type_c<int>;
    auto p = add_pointer(t);
    Note
    This is not exactly how the hana::type_c variable template is implemented because of some subtleties; things were dumbed down here for the sake of the explanation. Please check the reference for hana::type to know exactly what you can expect from a hana::type_c<...>.

    Benefits of this representation

    Но что это нам дает? Так как type_c<...> является просто объектом, мы можем хранить его в гетерогенной последовательности, такой как кортеж, мы можем перемещать его и передавать его (или возвращать его) функциям, и мы можем делать в основном все, что требует объекта:

    auto types = hana::make_tuple(hana::type_c<int*>, hana::type_c<char&>, hana::type_c<void>);
    auto char_ref = types[1_c];
    BOOST_HANA_CONSTANT_CHECK(char_ref == hana::type_c<char&>);
    Note
    Writing make_tuple(type_c<T>...) can be annoying when there are several types. For this reason, Hana provides the tuple_t<T...> variable template, which is syntactic sugar for make_tuple(type_c<T>...).

    Кроме того, обратите внимание, что, поскольку вышеупомянутый кортеж на самом деле является нормальной гетерогенной последовательностью, мы можем применять гетерогенные алгоритмы на этой последовательности так же, как мы могли бы, например, на кортеже int. Кроме того, поскольку мы просто манипулируем объектами, мы можем использовать полный язык вместо небольшого подмножества, доступного на уровне типов. Например, рассмотрим задачу удаления из последовательности типов всех типов, не являющихся ссылкой или указателем. С MPL нам пришлось бы использовать выражение заполнителя, чтобы выразить предикат, который является неуклюжим:

    using types = mpl::vector<int, char&, void*>;
    using ts = mpl::copy_if<types, mpl::or_<std::is_pointer<mpl::_1>,
    std::is_reference<mpl::_1>>>::type;
    // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    // placeholder expression
    static_assert(mpl::equal<ts, mpl::vector<char&, void*>>::value, "");

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

    auto types = hana::tuple_t<int*, char&, void>;
    auto ts = hana::filter(types, [](auto t) {
    return is_pointer(t) || is_reference(t);
    });
    BOOST_HANA_CONSTANT_CHECK(ts == hana::tuple_t<int*, char&>);

    Поскольку Хана обрабатывает все гетерогенные контейнеры равномерно, этот подход представления типов в качестве значений также имеет то преимущество, что теперь необходима одна библиотека как для гетерогенных вычислений, так и для вычислений уровня типов. Действительно, в то время как нам обычно нужны две разные библиотеки для выполнения почти одинаковых задач, нам теперь нужна одна библиотека. Опять же, рассмотрим задачу фильтрации последовательности с предикатом. С MPL и Fusion это то, что мы должны сделать:

    // types (MPL)
    using types = mpl::vector<int*, char&, void>;
    using ts = mpl::copy_if<types, mpl::or_<std::is_pointer<mpl::_1>,
    std::is_reference<mpl::_1>>>::type;
    // values (Fusion)
    auto values = fusion::make_vector(1, 'c', nullptr, 3.5);
    auto vs = fusion::filter_if<std::is_integral<mpl::_1>>(values);

    С Ханой требуется одна библиотека. Обратите внимание, как мы используем один и тот же алгоритм filter и один и тот же контейнер, и только настраиваем предикат, чтобы он мог работать на значениях:

    // types
    auto types = hana::tuple_t<int*, char&, void>;
    auto ts = hana::filter(types, [](auto t) {
    return is_pointer(t) || is_reference(t);
    });
    // values
    auto values = hana::make_tuple(1, 'c', nullptr, 3.5);
    auto vs = hana::filter(values, [](auto const& t) {
    return is_integral(hana::typeid_(t));
    });

    Но это еще не все. Действительно, наличие унифицированного синтаксиса для вычислений на уровне типов и значений позволяет достичь большей согласованности в интерфейсе гетерогенных контейнеров. Например, рассмотрим простую задачу создания гетерогенной карты, связывающей типы с значениями, а затем доступ к элементу ее. С Fusion происходящее далеко не очевидно для неподготовленного глаза:

    auto map = fusion::make_map<char, int, long, float, double, void>(
    "char", "int", "long", "float", "double", "void"
    );
    std::string Int = fusion::at_key<int>(map);

    Однако при унифицированном синтаксисе типов и значений одно и то же становится намного яснее:

    auto map = hana::make_map(
    hana::make_pair(hana::type_c<char>, "char"),
    hana::make_pair(hana::type_c<int>, "int"),
    hana::make_pair(hana::type_c<long>, "long"),
    hana::make_pair(hana::type_c<float>, "float"),
    hana::make_pair(hana::type_c<double>, "double")
    );
    std::string Int = map[hana::type_c<int>];

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

    Working with this representation

    До сих пор мы можем представлять типы в качестве значений и выполнять вычисления на уровне типов на этих объектах с использованием обычного синтаксиса C++. Это хорошо, но это не очень полезно, потому что у нас нет возможности получить нормальный тип C++ из представления объекта. Например, как мы можем объявить переменную, тип которой является результатом вычисления типа?

    auto t = add_pointer(hana::type_c<int>); // could be a complex type computation
    using T = the-type-represented-by-t;
    T var = ...;

    Прямо сейчас нет простого способа сделать это. Чтобы сделать это проще, мы обогащаем интерфейс контейнера basic_type, который мы определили выше. Вместо того, чтобы быть пустым структурой , мы теперь определяем его как

    template <typename T>
    struct basic_type {
    using type = T;
    };
    Note
    This is equivalent to making basic_type a metafunction in the MPL sense.

    Таким образом, мы можем использовать decltype, чтобы легко получить доступ к фактическому типу C++, представленному type_c<...> объект:

    auto t = add_pointer(hana::type_c<int>);
    using T = decltype(t)::type; // fetches basic_type<T>::type
    T var = ...;

    В целом, выполнение метапрограммирования на уровне типа с помощью Hana является трехступенчатым процессом:

    1. Представлять типы как объекты, обертывая ихhana::type_c<...>
    2. Выполнять преобразования типов с помощью синтаксиса значений
    3. Установить результатdecltype(...)::type

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

    auto t = hana::type_c<T>;
    auto result = huge_type_computation(t);
    using Result = decltype(result)::type;

    Кроме того, поскольку вы получаете преимущество работы с объектами (без необходимости обертывать / разворачивать) внутри вычисления, стоимость обертывания и распаковки амортизируется на всем вычислении. Следовательно, для вычислений сложных типов синтаксический шум этого трехступенчатого процесса быстро становится незначительным в свете увеличения выразительности работы со значениями внутри этого вычисления. Кроме того, использование значений вместо типов означает, что мы можем избежать ввода typename и template повсюду, что привело к большому количеству синтаксического шума в классическом метапрограммировании.

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

    auto t = hana::type_c<T>;
    auto result = type_computation(t);
    BOOST_HANA_CONSTANT_CHECK(is_pointer(result)); // third step skipped

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

    auto types = hana::tuple_t<int*, char&, void>; // first step skipped
    auto pointers = hana::transform(types, [](auto t) {
    return add_pointer(t);
    });

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

    template <typename ...T>
    struct smallest
    : mpl::deref<
    typename mpl::min_element<
    mpl::vector<T...>,
    mpl::less<mpl::sizeof_<mpl::_1>, mpl::sizeof_<mpl::_2>>
    >::type
    >
    { };
    template <typename ...T>
    using smallest_t = typename smallest<T...>::type;
    static_assert(std::is_same<
    smallest_t<char, long, long double>,
    char
    >::value, "");

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

    template <typename ...T>
    auto smallest = hana::minimum(hana::make_tuple(hana::type_c<T>...), [](auto t, auto u) {
    return hana::sizeof_(t) < hana::sizeof_(u);
    });
    template <typename ...T>
    using smallest_t = typename decltype(smallest<T...>)::type;
    static_assert(std::is_same<
    smallest_t<char, long, long double>, char
    >::value, "");

    Как вы можете видеть, синтаксический шум 3-ступенчатого процесса почти полностью скрыт остальной частью вычислений.

    The generic lifting process

    Первые вычисления уровня типа, которые мы ввели в виде функции, выглядели так:

    template <typename T>
    constexpr auto add_pointer(hana::basic_type<T> const&) {
    return hana::type<T*>;
    }

    Хотя это выглядит более сложным, мы также можем написать его как:

    template <typename T>
    constexpr auto add_pointer(hana::basic_type<T> const&) {
    return hana::type_c<typename std::add_pointer<T>::type>;
    }

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

    template <typename T>
    constexpr auto add_const(hana::basic_type<T> const&)
    { return hana::type_c<typename std::add_const<T>::type>; }
    template <typename T>
    constexpr auto add_volatile(hana::basic_type<T> const&)
    { return hana::type_c<typename std::add_volatile<T>::type>; }
    template <typename T>
    constexpr auto add_lvalue_reference(hana::basic_type<T> const&)
    { return hana::type_c<typename std::add_lvalue_reference<T>::type>; }
    // etc...

    Это механическое преобразование легко абстрагировать в общий лифтер, который может обрабатывать любую MPL метафункцию следующим образом:

    template <template <typename> class F, typename T>
    constexpr auto metafunction(hana::basic_type<T> const&)
    { return hana::type_c<typename F<T>::type>; }
    auto t = hana::type_c<int>;
    BOOST_HANA_CONSTANT_CHECK(metafunction<std::add_pointer>(t) == hana::type_c<int*>);

    В более общем плане мы хотим разрешить метафункции с любым количеством аргументов, что приводит нас к следующей менее наивной реализации:

    template <template <typename ...> class F, typename ...T>
    constexpr auto metafunction(hana::basic_type<T> const& ...)
    { return hana::type_c<typename F<T...>::type>; }
    metafunction<std::common_type>(hana::type_c<int>, hana::type_c<long>) == hana::type_c<long>
    );

    Хана предоставляет аналогичный универсальный метафункциональный лифтер под названием hana::metafunction. Одно небольшое улучшение заключается в том, что hana::metafunction является функциональным объектом вместо перегруженной функции, поэтому его можно передать алгоритмам более высокого порядка. Это также модель чуть более мощного концепта Метафункция, но пока это можно смело игнорировать. Процесс, который мы исследовали в этом разделе, относится не только к метафункциям, но и к шаблонам. Действительно, мы могли бы определить:

    template <template <typename ...> class F, typename ...T>
    constexpr auto template_(hana::basic_type<T> const& ...)
    { return hana::type_c<F<T...>>; }
    template_<std::vector>(hana::type_c<int>) == hana::type_c<std::vector<int>>
    );

    Hana предоставляет общий лифтер для шаблонов под названием hana::template_, а также общий лифтер для MPL MetafunctionClasses под названием hana::metafunction_class. Это дает нам возможность единообразно представлять в качестве функций вычисления уровня типа «наследие», так что любой код, написанный с использованием классической библиотеки метапрограммирования уровня типа, может почти тривиально использоваться с Ханой. Например, скажем, у вас есть большой кусок кода на основе MPL, и вы хотите взаимодействовать с Ханой. Это не сложнее, чем обернуть ваши метафункции с помощью лифтера, предоставленного Ханой:

    template <typename T>
    struct legacy {
    using type = ...; // something you really don't want to mess with
    };
    auto types = hana::make_tuple(...);
    auto use = hana::transform(types, hana::metafunction<legacy>);

    Тем не менее, обратите внимание, что не все вычисления на уровне типов могут быть отменены с помощью инструментов, предоставляемых Hana. Например, std::extent не может быть снят, потому что он требует нетиповых параметров шаблона. Поскольку в C++ нет единого способа работы с нетиповыми шаблонными параметрами, необходимо прибегнуть к использованию рукописного функционального объекта, специфичного для вычислений на уровне типов:

    auto extent = [](auto t, auto n) {
    return std::extent<typename decltype(t)::type, hana::value(n)>{};
    };
    BOOST_HANA_CONSTANT_CHECK(extent(hana::type_c<char>, hana::int_c<1>) == hana::size_c<0>);
    BOOST_HANA_CONSTANT_CHECK(extent(hana::type_c<char[1][2]>, hana::int_c<1>) == hana::size_c<2>);
    Note
    Do not forget to include the bridge header for std::integral_constants (<boost/hana/ext/std/integral_constant.hpp>) when using type traits from <type_traits> directly.

    На практике, однако, это не должно быть проблемой, поскольку подавляющее большинство вычислений на уровне типов может быть легко снято. Наконец, поскольку метафункции, предоставляемые заголовком , используются так часто, Hana предоставляет улучшенную версию для каждого из них. Эти поднятые черты находятся в пространстве имен hana::traits, и они живут в заголовке <boost/hana/traits.hpp>:

    BOOST_HANA_CONSTANT_CHECK(hana::traits::add_pointer(hana::type_c<int>) == hana::type_c<int*>);
    BOOST_HANA_CONSTANT_CHECK(hana::traits::common_type(hana::type_c<int>, hana::type_c<long>) == hana::type_c<long>);
    BOOST_HANA_CONSTANT_CHECK(hana::traits::is_integral(hana::type_c<int>));
    auto types = hana::tuple_t<int, char, long>;
    BOOST_HANA_CONSTANT_CHECK(hana::all_of(types, hana::traits::is_integral));

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

    Note
    Curious or skeptical readers should consider checking the minimal reimplementation of the MPL presented in the appendices.

    Introspection


    Статическая интроспекция, как мы обсудим это здесь, это способность программы исследовать тип объекта во время компиляции. Другими словами, это программный интерфейс для взаимодействия с типами во время компиляции. Например, вы когда-нибудь хотели проверить, есть ли у какого-то неизвестного типа член с именем foo? Или, возможно, в какой-то момент вам нужно было повторить на членах структуры ?

    struct Person {
    std::string name;
    int age;
    };
    Person john{"John", 30};
    for (auto& member : john)
    std::cout << member.name << ": " << member.value << std::endl;
    // name: John
    // age: 30

    Если вы написали несколько шаблонов в своей жизни, велика вероятность того, что вы столкнулись с первой проблемой проверки участника. Кроме того, любой, кто пытался реализовать сериализацию объектов или даже просто красивую печать, столкнулся со второй проблемой. В большинстве динамических языков, таких как Python, Ruby или JavaScript, эти проблемы полностью решены, и интроспекция используется каждый день программистами, чтобы упростить множество задач. Однако, как программист C++, у нас нет языковой поддержки для этих вещей, что делает несколько задач намного сложнее, чем они должны быть. Хотя для правильного решения этой проблемы, вероятно, потребуется языковая поддержка, Хана делает некоторые общие схемы интроспекции гораздо более доступными.

    Checking expression validity

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

    template <typename T>
    std::string optionalToString(T const& obj) {
    if (obj.toString() is a valid expression)
    return obj.toString();
    else
    return "toString not defined";
    }
    Note
    While most use cases for this technique will be addressed by concepts lite in future revisions of the standard, there will still be cases where a quick and dirty check is more convenient than creating a full blown concept.

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

    template <typename T, typename = void>
    struct has_toString
    : std::false_type
    { };
    template <typename T>
    struct has_toString<T, decltype((void)std::declval<T>().toString())>
    : std::true_type
    { };

    Это работает, но цель не очень ясна, и большинство людей без глубокого знания шаблонного метапрограммирования подумают, что это черная магия. Затем мы можем реализовать optionalToString как

    template <typename T>
    std::string optionalToString(T const& obj) {
    return obj.toString();
    else
    return "toString not defined";
    }
    Note
    Of course, this implementation won't actually work because both branches of the if statement will be compiled. If obj does not have a toString method, the compilation of the if branch will fail. We will address this issue in a moment.

    Вместо вышеупомянутого трюка SFINAE Хана предоставляет функцию is_valid, которая может быть объединена с C++14 общими лямбдами , чтобы получить гораздо более чистую реализацию того же самого:

    auto has_toString = hana::is_valid([](auto&& obj) -> decltype(obj.toString()) { });

    Это оставляет нам функциональный объект has_toString, который возвращает, действительно ли данное выражение на аргументе, который мы передаем ему. Результат возвращается как IntegralConstant, поэтому constexpr-ness здесь не проблема, поскольку результат функции представлен как тип в любом случае. Теперь, в дополнение к тому, чтобы быть менее многословным (это один лайнер!), намерение намного яснее. Другие преимущества заключаются в том, что has_toString может быть передан алгоритмам более высокого порядка, а также может быть определен в области функций, поэтому нет необходимости загрязнять область пространства имен деталями реализации. Вот как мы теперь напишем optionalToString:

    template <typename T>
    std::string optionalToString(T const& obj) {
    if (has_toString(obj))
    return obj.toString();
    else
    return "toString not defined";
    }

    Намного чище, да? Однако, как мы уже говорили ранее, эта реализация на самом деле не будет работать, потому что обе ветви , если Всегда должен быть составлен, независимо от того, имеет ли obj метод toString. Существует несколько возможных вариантов, но наиболее классическим является использование std::enable_if:

    template <typename T>
    auto optionalToString(T const& obj)
    -> std::enable_if_t<decltype(has_toString(obj))::value, std::string>
    { return obj.toString(); }
    template <typename T>
    auto optionalToString(T const& obj)
    -> std::enable_if_t<decltype(!has_toString(obj))::value, std::string>
    { return "toString not defined"; }
    Note
    We're using the fact that has_toString returns an IntegralConstant to write decltype(...)::value, which is a constant expression. For some reason, has_toString(obj) is not considered a constant expression, even though I think it should be one because we never read from obj (see the section on advanced constexpr).

    Хотя эта реализация совершенно верна, она все еще довольно громоздка, потому что требует написания двух различных функций и явного прохождения обручей SFINAE с помощью std::enable_if. Однако, как вы помните из раздела ветвление времени компиляции , Hana предоставляет функцию if_, которая может использоваться для эмуляции функциональности static_if. Вот как можно написать optionalToString с hana::if_:

    template <typename T>
    std::string optionalToString(T const& obj) {
    return hana::if_(has_toString(obj),
    [](auto& x) { return x.toString(); },
    [](auto& x) { return "toString not defined"; }
    )(obj);
    }

    В предыдущем примере рассматривался только конкретный случай проверки наличия нестатической функции-члена. Тем не менее, is_valid можно использовать для определения достоверности практически любого вида выражения. Для полноты мы представляем список распространенных случаев использования для проверки действительности, а также способы использования is_valid для их реализации.

    Non-static members

    Первая идиома, которую мы рассмотрим, это проверка наличия нестатического члена. Мы можем сделать это так же, как и в предыдущем примере:

    auto has_member = hana::is_valid([](auto&& x) -> decltype((void)x.member) { });
    struct Foo { int member[4]; };
    struct Bar { };
    BOOST_HANA_CONSTANT_CHECK(has_member(Foo{}));
    BOOST_HANA_CONSTANT_CHECK(!has_member(Bar{}));

    Обратите внимание, как мы отбрасываем результат x.member в void? Это делается для того, чтобы убедиться, что наше обнаружение также работает для типов, которые не могут быть возвращены из функций, таких как типы массивов. Кроме того, важно использовать ссылку в качестве параметра к нашей общей лямбде, потому что в противном случае потребуется x, чтобы быть CopyConstructible, что не то, что мы пытаемся проверить. Этот подход прост и удобен, когда объект доступен. Однако, когда шашка предназначена для использования без какого-либо объекта вокруг, может быть лучше приспособлена следующая альтернативная реализация:

    auto has_member = hana::is_valid([](auto t) -> decltype(
    (void)hana::traits::declval(t).member
    ) { });
    struct Foo { int member[4]; };
    struct Bar { };
    BOOST_HANA_CONSTANT_CHECK(has_member(hana::type_c<Foo>));
    BOOST_HANA_CONSTANT_CHECK(!has_member(hana::type_c<Bar>));

    Эта проверка достоверности отличается от того, что мы видели ранее, потому что родовая лямбда больше не ожидает обычного объекта; теперь она ожидает тип (который является объектом, но все еще представляет тип). Затем мы используем заголовок hana::traits::declval lifted metafunction из <boost/hana/traits.hpp>, чтобы создать значение r типа, представленного t, которое мы затем можем использовать для проверки на нестатический элемент. Наконец, вместо передачи реального объекта has_member (например, Foo{} или Bar{}), мы теперь передаем type_c<...>. Эта реализация идеально подходит, когда вокруг нет объекта.

    Static members

    Проверка на статический член проста, и для полноты предусмотрена:

    auto has_member = hana::is_valid([](auto t) -> decltype(
    (void)decltype(t)::type::member
    ) { });
    struct Foo { static int member[4]; };
    struct Bar { };
    BOOST_HANA_CONSTANT_CHECK(has_member(hana::type_c<Foo>));
    BOOST_HANA_CONSTANT_CHECK(!has_member(hana::type_c<Bar>));

    Опять же, мы ожидаем, что тип будет передан в шашку. Внутри общей лямбды мы используем decltype(t)::type для получения фактического типа C++, представленного объектом t, как объяснено в разделе type computations. Затем мы забираем статический элемент внутри этого типа и отбрасываем его на void по той же причине, что и для нестатических членов.

    Nested type names

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

    auto has_member = hana::is_valid([](auto t) -> hana::type<
    typename decltype(t)::type::member
    //^^^^^^^^ needed because of the dependent context
    > { });
    struct Foo { struct member; /* not defined! */ };
    struct Bar { };
    BOOST_HANA_CONSTANT_CHECK(has_member(hana::type_c<Foo>));
    BOOST_HANA_CONSTANT_CHECK(!has_member(hana::type_c<Bar>));

    Можно задаться вопросом, почему мы используем -> hana::type вместо просто -> typename-expression. Опять же, причина в том, что мы хотим поддерживать типы, которые не могут быть возвращены из функций, таких как типы массивов или неполные типы.

    Nested templates

    Проверка вложенного имени шаблона аналогична проверке вложенного имени типа, за исключением того, что мы используем template_<...> переменный шаблон вместо type<...> в общей лямбде:

    auto has_member = hana::is_valid([](auto t) -> decltype(hana::template_<
    decltype(t)::type::template member
    // ^^^^^^^^ needed because of the dependent context
    >) { });
    struct Foo { template <typename ...> struct member; };
    struct Bar { };
    BOOST_HANA_CONSTANT_CHECK(has_member(hana::type_c<Foo>));
    BOOST_HANA_CONSTANT_CHECK(!has_member(hana::type_c<Bar>));

    Taking control of SFINAE

    Делать что-то только в том случае, если выражение хорошо сформировано, является очень распространенным шаблоном в C++. Действительно, функция optionalToString является лишь одним из примеров следующего шаблона, который является очень общим:

    template <typename T>
    auto f(T x) {
    if (some expression involving x is well-formed)
    return something involving x;
    else
    return something else;
    }

    Чтобы инкапсулировать этот шаблон, Хана предоставляет функцию sfinae, которая позволяет выполнять выражение, но только если оно хорошо сформировано:

    auto maybe_add = hana::sfinae([](auto x, auto y) -> decltype(x + y) {
    return x + y;
    });
    maybe_add(1, 2); // hana::just(3)
    std::vector<int> v;
    maybe_add(v, "foobar"); // hana::nothing

    Здесь мы создаем функцию maybe_add, которая является просто общей лямбдой, обернутой функцией Ханы sfinae. maybe_add - функция, которая принимает два входа и возвращает just результат родовой лямбды, если этот вызов хорошо сформирован, и ничего в противном случае. just(...) и nothing относятся к типу контейнера, называемому hana::optional, который по существу представляет собой время компиляции std::optional. В целом, maybe_add морально эквивалентен следующей функции, возвращающей std::optional, за исключением того, что проверка выполняется в момент компиляции:

    auto maybe_add = [](auto x, auto y) {
    if (x + y is well formed)
    return std::optional<decltype(x + y)>{x + y};
    else
    return std::optional<???>{};
    };

    Получается, что мы можем воспользоваться sfinae и optional для реализации функции optionalToString следующим образом:

    template <typename T>
    std::string optionalToString(T const& obj) {
    auto maybe_toString = hana::sfinae([](auto&& x) -> decltype(x.toString()) {
    return x.toString();
    });
    return maybe_toString(obj).value_or("toString not defined");
    }

    Во-первых, мы обертываем в String с функцией sfinae. Следовательно, Maybe_toString является функцией, которая либо возвращает just(x.toString()), если это хорошо сформировано, либо ничего в противном случае. Во-вторых, мы используем функцию .value_or() для извлечения необязательного значения из контейнера. Если необязательное значение ничто, .value_or() возвращает заданное ему значение по умолчанию; в противном случае оно возвращает значение внутри just (здесь x.toString()). Этот способ рассматривать SFINAE как особый случай вычислений, которые могут потерпеть неудачу, очень чистый и мощный, тем более что функции sfinae'd могут быть объединены через hana::optional Monad, который остается в справочной документации.

    Introspecting user-defined types

    Вы когда-нибудь хотели повторить над членами пользовательского типа? Цель этого раздела - показать вам, как Хана может быть использована, чтобы сделать это довольно легко. Чтобы работать с определенными пользователями типами, Хана определяет концепцию Struct. Как только определяемый пользователем тип является моделью этого понятия, можно повторить над членами объекта этого типа и запросить другую полезную информацию. Чтобы превратить тип, определенный пользователем, в Struct, доступны несколько вариантов. Во-первых, вы можете определить членов вашего пользовательского типа с помощью макроса BOOST_HANA_DEFINE_STRUCT:

    struct Person {
    BOOST_HANA_DEFINE_STRUCT(Person,
    (std::string, name),
    (int, age)
    );
    };

    Этот макрос определяет два члена (имя и возраст ) с заданными типами. Затем он определяет некоторый шаблон внутри Person::hana, вложенный struct, который требуется, чтобы сделать Person моделью концепции Struct. Конструкторы не определены (так что POD-ness сохраняется), члены определены в том же порядке, что и здесь, и макрос можно использовать с шаблоном structs так же хорошо и в любом объеме. Также обратите внимание, что вы можете добавить больше участников в Person Введите после или перед использованием макроса. Тем не менее, только участники, определенные с макросом, будут подобраны при самоанализе типа Лицо . Достаточно легко? Теперь к Person можно получить доступ программно:

    Person john{"John", 30};
    hana::for_each(john, [](auto pair) {
    std::cout << hana::to<char const*>(hana::first(pair)) << ": "
    << hana::second(pair) << std::endl;
    });
    // name: John
    // age: 30

    Итерация по Struct выполняется так, как если бы Struct представлял собой последовательность пар, где первый элемент пары является ключом, связанным с членом, а второй элемент является самим членом. Когда Struct определяется через макрос BOOST_HANA_DEFINE_STRUCT, ключом, связанным с любым членом, является время компиляции hana::string, представляющее имя этого члена. Вот почему функция используется с для каждого принимает один аргумент pair, а затем использует first и second для доступа к подчасти пары. Кроме того, обратите внимание, как функция to используется на имя участника? Это преобразует строку времени компиляции в constexpr char const*, чтобы она могла couted. Поскольку всегда можно использовать первый и второй , чтобы получить подчасти пары, мы также можем использовать функцию fuse , чтобы обернуть нашу лямбду и сделать ее бинарной лямбдой:

    hana::for_each(john, hana::fuse([](auto name, auto member) {
    std::cout << hana::to<char const*>(name) << ": " << member << std::endl;
    }));

    Теперь он выглядит намного чище. Как мы только что упомянули, Структура рассматривается как своего рода последовательность пар с целью итерации. На самом деле, Struct можно даже искать как ассоциативную структуру данных, ключами которой являются имена членов и значениями которых являются сами члены:

    std::string name = hana::at_key(john, "name"_s);
    BOOST_HANA_RUNTIME_CHECK(name == "John");
    int age = hana::at_key(john, "age"_s);
    Note
    The _s user-defined literal creates a compile-time hana::string. It is located in the boost::hana::literals namespace. Note that it is not part of the standard yet, but it is supported by Clang and GCC. If you want to stay 100% standard, you can use the BOOST_HANA_STRING macro instead.

    Основное различие между Struct и hana::map заключается в том, что карта может быть изменена (ключи могут быть добавлены и удалены), в то время как Struct неизменен. Тем не менее, вы можете легко преобразовать Struct в hana::map с to, а затем вы можете манипулировать им более гибким способом.

    auto map = hana::insert(hana::to<hana::map_tag>(john), hana::make_pair("last name"_s, "Doe"s));
    std::string name = map["name"_s];
    BOOST_HANA_RUNTIME_CHECK(name == "John");
    std::string last_name = map["last name"_s];
    BOOST_HANA_RUNTIME_CHECK(last_name == "Doe");
    int age = map["age"_s];

    Использование макроса BOOST_HANA_DEFINE_STRUCT для адаптации структуры удобно, но иногда нельзя изменить тип, который необходимо адаптировать. В этих случаях макрос BOOST_HANA_ADAPT_STRUCT может быть использован для адаптации структуры специальным образом:

    namespace not_my_namespace {
    struct Person {
    std::string name;
    int age;
    };
    }
    BOOST_HANA_ADAPT_STRUCT(not_my_namespace::Person, name, age);
    Note
    The BOOST_HANA_ADAPT_STRUCT macro must be used at global scope.

    Эффект точно такой же, как и в макросе BOOST_HANA_DEFINE_STRUCT, за исключением того, что вам не нужно изменять тип, который вы хотите адаптировать. Наконец, можно также определить пользовательские аксессуары, используя макрос BOOST_HANA_ADAPT_ADT:

    namespace also_not_my_namespace {
    struct Person {
    std::string get_name();
    int get_age();
    };
    }
    BOOST_HANA_ADAPT_ADT(also_not_my_namespace::Person,
    (name, [](auto const& p) { return p.get_name(); }),
    (age, [](auto const& p) { return p.get_age(); })
    );

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

    Example: generating JSON

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

    struct Car {
    BOOST_HANA_DEFINE_STRUCT(Car,
    (std::string, brand),
    (std::string, model)
    );
    };
    struct Person {
    BOOST_HANA_DEFINE_STRUCT(Person,
    (std::string, name),
    (std::string, last_name),
    (int, age)
    );
    };
    Car bmw{"BMW", "Z3"}, audi{"Audi", "A4"};
    Person john{"John", "Doe", 30};
    auto tuple = hana::make_tuple(john, audi, bmw);
    std::cout << to_json(tuple) << std::endl;

    И выход, пройдя через симпатичный принтер JSON, должен выглядеть как

    1 [
    2  {
    3  "name": "John",
    4  "last_name": "Doe",
    5  "age": 30
    6  },
    7  {
    8  "brand": "Audi",
    9  "model": "A4"
    10  },
    11  {
    12  "brand": "BMW",
    13  "model": "Z3"
    14  }
    15 ]

    Во-первых, давайте определим несколько полезных функций, чтобы упростить манипулирование строками:

    template <typename Xs>
    std::string join(Xs&& xs, std::string sep) {
    return hana::fold(hana::intersperse(std::forward<Xs>(xs), sep), "", hana::_ + hana::_);
    }
    std::string quote(std::string s) { return "\"" + s + "\""; }
    template <typename T>
    auto to_json(T const& x) -> decltype(std::to_string(x)) {
    return std::to_string(x);
    }
    std::string to_json(char c) { return quote({c}); }
    std::string to_json(std::string s) { return quote(s); }

    Перегрузки quote и to_json довольно понятны. Однако для функции join может потребоваться некоторое объяснение. Функция интерсперс берет последовательность и разделитель и возвращает новую последовательность с разделителем между каждой парой элементов исходной последовательности. Другими словами, мы берем последовательность формы [x1,..., xn] и превращаем ее в последовательность формы [x1, seq, x2, sep,..., sep, xn]. Наконец, мы складываем полученную последовательность с объектом функции _+_, что эквивалентно std::plus<>{}. Поскольку наша последовательность содержит std::strings (мы предполагаем, что это так), это имеет эффект объединения всех строк последовательности в одну большую строку. Теперь давайте определим, как напечатать Последовательность:

    template <typename Xs>
    std::string> to_json(Xs const& xs) {
    auto json = hana::transform(xs, [](auto const& x) {
    return to_json(x);
    });
    return "[" + join(std::move(json), ", ") + "]";
    }

    Во-первых, мы используем алгоритм transform, чтобы превратить нашу последовательность объектов в последовательность std::strings в формате JSON. Затем мы присоединяем эту последовательность к запятым и заключаем ее в [] для обозначения последовательности в обозначении JSON. Достаточно просто? Теперь давайте посмотрим, как печатать пользовательские типы:

    template <typename T>
    std::string> to_json(T const& x) {
    auto json = hana::transform(hana::keys(x), [&](auto name) {
    auto const& member = hana::at_key(x, name);
    return quote(hana::to<char const*>(name)) + " : " + to_json(member);
    });
    return "{" + join(std::move(json), ", ") + "}";
    }

    Здесь мы используем метод keys для извлечения tuple, содержащего имена членов определяемого пользователем типа. Затем мы трансформируем эту последовательность в последовательность "имя" : член строки, которые мы затем присоединяем и заключаем с {}, который используется для обозначения объектов в обозначении JSON. И это все!

    Generalities on containers


    В этом разделе объясняется несколько важных понятий о контейнерах Ханы: как их создавать, срок службы их элементов и другие заботы.

    Container creation

    В то время как обычным способом создания объекта в C++ является использование его конструктора, гетерогенное программирование делает вещи немного сложнее. Действительно, в большинстве случаев человек не интересуется (или даже не знает) фактическим типом создаваемого разнородного контейнера. В других случаях можно было бы написать этот тип явно, но это было бы избыточно или обременительно. По этой причине Хана использует другой подход, заимствованный из std::make_tuple, для создания новых контейнеров. Подобно тому, как можно создать std::tuple с std::make_tuple, hana::tuple можно создать с hana::make_tuple. Однако в более общем плане контейнеры в Хане могут быть созданы с функцией make:

    auto xs = hana::make<hana::tuple_tag>(1, 2.2, 'a', "bcde"s);

    На самом деле, make_tuple - это просто сокращение для make, поэтому вам не нужно набирать boost::hana:::hana:::tuple_tag> Когда ты выходишь из пространства имён Ханы. Проще говоря, make<...> используется по всей библиотеке для создания различных типов объектов, тем самым обобщая семейство функций std:::make_xxx. Например, можно создать hana::range целых чисел времени компиляции с make:

    constexpr auto r = hana::make<hana::range_tag>(hana::int_c<3>, hana::int_c<10>);
    static_assert(r == hana::make_range(hana::int_c<3>, hana::int_c<10>), "");

    Эти типы со следящим _tag являются фиктивными типами , представляющими семейство гетерогенных контейнеров (hana::tuple, hana::map и т.д.). Теги задокументированы в разделе Ядро Ханы .

    Для удобства, когда компонент Hana предоставляет функцию make, он также предоставляет ярлык make_xxx для уменьшения набора текста. Также интересным моментом, который можно поднять в этом примере, является тот факт, что r является constexpr. В общем случае, когда контейнер инициализируется только с постоянными выражениями (что имеет место для r), этот контейнер может быть помечен как constexpr.

    До сих пор мы создавали контейнеры только с семейством функций make_xxx. Однако некоторые контейнеры предоставляют конструкторы как часть интерфейса. Например, можно создать hana::tuple Точно так же, как можно создать std::tuple:

    hana::tuple<int, double, char, std::string> xs{1, 2.2, 'a', "bcde"s};

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

    hana::range<???> xs{hana::int_c<3>, hana::int_c<10>};

    Фактически, мы даже не можем указать тип объекта, который мы хотели бы создать в этом случае, потому что точный тип hana::range определяется реализацией, что приводит нас к следующему разделу.

    Container types

    Цель этого раздела - уточнить, что можно ожидать от типов контейнеров Ханы. Действительно, до сих пор мы всегда позволяли компилятору выводить фактический тип контейнеров, используя семейство функций make_xxx вместе с auto. Но в целом, что можно сказать о типе контейнера?

    auto xs = hana::make_tuple(1, '2', "345");
    auto ints = hana::make_range(hana::int_c<0>, hana::int_c<100>);
    // what can we say about the types of `xs` and `ints`?

    Ответ заключается в том, что это зависит. Некоторые контейнеры имеют четко определенные типы, в то время как другие не указывают их представление. В этом примере тип объекта, возвращаемого make_tuple, хорошо определен, в то время как тип, возвращаемый make_range, определяется реализацией:

    hana::tuple<int, char, char const*> xs = hana::make_tuple(1, '2', "345");
    auto ints = hana::make_range(hana::int_c<0>, hana::int_c<100>);
    // can't specify the type of ints, however

    Это документируется на основе каждого контейнера; когда контейнер имеет представление, определенное реализацией, примечание, объясняющее, что именно можно ожидать от этого представления, включено в описание контейнера. Существует несколько причин, по которым представление контейнера не указывается; они объясняются в rationales. Когда представление контейнера определяется имплементацией, следует быть осторожным, чтобы не делать никаких предположений об этом, если только эти предположения явно не разрешены в документации контейнера.

    Overloading on container types

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

    template <typename T>
    void f(std::vector<T> xs) {
    // ...
    }
    template <typename ...???>
    void f(unspecified-range-type<???> r) {
    // ...
    }

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

    template <typename T>
    void f(std::vector<T> xs) {
    // ...
    }
    template <typename R, typename = std::enable_if_t<hana::is_a<hana::range_tag, R>()>>
    void f(R r) {
    // ...
    }

    Таким образом, вторая перегрузка f будет совпадать только тогда, когда R является типом, меткой которого является range_tag, независимо от точного представления этого диапазона. Конечно, is_a можно использовать с любым контейнером: tuple, map, set и так далее.

    Container elements

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

    std::string hello = "Hello";
    std::vector<char> world = {'W', 'o', 'r', 'l', 'd'};
    // hello is copied, world is moved-in
    auto xs = hana::make_tuple(hello, std::move(world));
    // s is a reference to the copy of hello inside xs.
    // It becomes a dangling reference as soon as xs is destroyed.
    std::string& s = xs[0_c];

    Как и контейнеры в стандартной библиотеке, контейнеры в Хане ожидают, что их элементы будут объектами. По этой причине ссылки могут не храниться в них . Когда ссылки должны храниться внутри контейнера, вместо этого следует использовать std::reference_wrapper:

    std::vector<int> ints = { /* huge vector of ints */ };
    std::vector<std::string> strings = { /* huge vector of strings */ };
    auto map = hana::make_map(
    hana::make_pair(hana::type_c<int>, std::ref(ints)),
    hana::make_pair(hana::type_c<std::string>, std::ref(strings))
    );
    auto& v = map[hana::type_c<int>].get();

    Generalities on algorithms


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

    By-value semantics

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

    auto to_str = [](auto const& x) {
    std::stringstream ss;
    ss << x;
    return ss.str();
    };
    auto xs = hana::make_tuple(1, 2.2, 'a', "bcde");
    hana::reverse(hana::transform(xs, to_str)) == hana::make_tuple("bcde", "a", "2.2", "1")
    );

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

    Можно также подумать, что возвращение полных последовательностей, владеющих элементами алгоритма, приведет к тоннам нежелательных копий. Например, при использовании reverse и transform можно подумать, что промежуточная копия сделана после вызова transform:

    hana::transform(xs, to_str) // <-- copy into reverse(...) here?
    );

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

    hana::transform(xs, to_str) // <-- nope, move from the temporary instead!
    );

    В конечном счете, цель состоит в том, чтобы код, написанный с использованием Ханы, был эквивалентен умному рукописному коду, за исключением того, что он должен быть приятным для написания. Соображения эффективности подробно разъясняются в их собственном разделе 1>.

    (Non-)Laziness

    Алгоритмы в Хане не ленивы. Когда алгоритм называется, он выполняет свою работу и возвращает новую последовательность, содержащую результат, конец истории. Например, назвать алгоритм пермутаций на большой последовательности — глупая идея, потому что Хана фактически вычислит все перестановки:

    auto perms = hana::permutations(hana::make_tuple(1, 2, 3, 4, 5, 6, 7, 8, 9, 10));
    // perms has 3 628 800 elements, and your compiler just crashed

    Алгоритмы в Boost. Fusion возвращает представления, которые содержат исходную последовательность по ссылке и применяют алгоритм по требованию, поскольку элементы последовательности доступны. Это приводит к тонким жизненным проблемам, таким как представление, которое относится к последовательности, которая была разрушена. Дизайн Ханы предполагает, что большую часть времени мы хотим получить доступ ко всем или почти всем элементам в последовательности в любом случае, и, следовательно, производительность не является большим аргументом в пользу лени.

    What is generated?

    Алгоритмы в Хане немного особенные по отношению к коду времени выполнения, в который они расширены. Цель этого подраздела состоит не в том, чтобы объяснить, какой именно код генерируется, который в любом случае зависит от компилятора, а в том, чтобы дать представление о вещах. По сути, алгоритм Хана похож на незавершенную версию эквивалентного классического алгоритма. Действительно, поскольку границы обработанной последовательности известны во время компиляции, имеет смысл, что мы можем развернуть цикл по последовательности. Например, рассмотрим для каждого алгоритм:

    auto xs = hana::make_tuple(0, 1, 2, 3);

    Если бы xs был последовательностью времени выполнения вместо кортежа, его длина была бы известна только во время выполнения, и вышеупомянутый код должен был бы быть реализован как цикл:

    for (int i = 0; i < xs.size(); ++i) {
    f(xs[i]);
    }

    Однако в нашем случае длина последовательности известна во время компиляции, и поэтому нам не нужно проверять индекс при каждой итерации. Поэтому мы можем просто написать:

    f(xs[0_c]);
    f(xs[1_c]);
    f(xs[2_c]);
    f(xs[3_c]);

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

    for (??? i = 0_c; i < xs.size(); ++i) {
    f(xs[i]);
    }

    Side effects and purity

    По умолчанию Хана принимает функции за чистые. Чистая функция — это функция, которая вообще не имеет побочных эффектов. Другими словами, это функция, влияние которой на программу определяется исключительно ее обратным значением. В частности, такая функция может не иметь доступа к любому состоянию, которое пережило бы одно вызов функции. Эти функции обладают очень хорошими свойствами, такими как способность математически рассуждать о них, переупорядочение или даже устранение вызовов и так далее. За исключением случаев, когда указано иное, все функции, используемые в алгоритмах более высокого порядка, должны быть чистыми. В частности, функции, передаваемые алгоритмам более высокого порядка, не гарантированно будут называться каким-либо конкретным числом раз. Кроме того, порядок исполнения, как правило, не уточняется и поэтому не должен восприниматься как должное. Если это отсутствие гарантий относительно вызовов функции кажется сумасшедшим, рассмотрите следующее использование any_of алгоритм:

    auto r = hana::any_of(hana::make_tuple("hello"s, 1.2, 3), [](auto x) {
    return std::is_integral<decltype(x)>{};
    });
    Note
    For this to work, the external adapters for std::integral_constant contained in <boost/hana/ext/std/integral_constant.hpp> must be included.

    Согласно предыдущему разделу, этот алгоритм должен быть расширен в нечто вроде:

    auto xs = hana::make_tuple("hello"s, 1.2, 3);
    auto pred = [](auto x) { return std::is_integral<decltype(x)>{}; };
    auto r = hana::bool_c<
    pred(xs[0_c]) ? true :
    pred(xs[1_c]) ? true :
    pred(xs[2_c]) ? true :
    false
    >;

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

    auto xs = hana::make_tuple("hello"s, 1.2, 3);
    auto pred = [](auto x) { return std::is_integral<decltype(x)>{}; };
    auto r = hana::bool_c<
    decltype(pred(xs[0_c]))::value ? true :
    decltype(pred(xs[1_c]))::value ? true :
    decltype(pred(xs[2_c]))::value ? true :
    false
    >;
    Note
    As you will be able to deduce from the next section on cross-phase computations, the implementation of any_of must actually be more general than this. However, this lie-to-children is perfect for educational purposes.

    Как видите, предикат никогда даже не исполняется, используется только его тип результата на конкретном объекте. Что касается порядка оценки, рассмотрите алгоритм transform, который указан (для кортежей):

    hana::transform(hana::make_tuple(x1, ..., xn), f) == hana::make_tuple(f(x1), ..., f(xn))

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

    Cross-phase algorithms

    В этом разделе представлено понятие кросс-фазных вычислений и алгоритмов. Фактически, мы уже использовали кросс-фазные алгоритмы в быстром старте, например, с фильтром, но мы не объяснили точно, что происходило в то время. Но прежде чем мы введем кросс-фазные алгоритмы, давайте определим, что мы имеем в виду под кросс-фазой . Фазы, о которых мы здесь говорим, это компиляция и выполнение программы. В C++, как и в большинстве статически типизированных языков, существует четкое различие между временем компиляции и временем выполнения. Когда мы говорим о межфазных вычислениях, мы имеем в виду вычисления, которые каким-то образом выполняются на этих этапах, т.е. частично выполняются во время компиляции и частично выполняются во время выполнения.

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

    struct Fish { std::string name; };
    struct Cat { std::string name; };
    struct Dog { std::string name; };
    auto animals = hana::make_tuple(Fish{"Nemo"}, Cat{"Garfield"}, Dog{"Snoopy"});
    // ^^^^^^^ not a compile-time value
    BOOST_HANA_CONSTANT_CHECK(hana::length(animals) == hana::size_c<3>);
    // ^^^^^^^^^^^^^^^^^^^^^ assertion done at compile-time

    Очевидно, что кортеж не может быть выполнен constexpr, так как он содержит время выполнения std::strings. Тем не менее, несмотря на то, что он не называется постоянным выражением, длина возвращает то, что может быть использовано во время компиляции. Если вы думаете об этом, размер кортежа известен во время компиляции независимо от его содержания, и поэтому имеет смысл, чтобы эта информация была доступна нам во время компиляции. Если это кажется удивительным, подумайте о std::tuple и std::tuple_size:

    std::tuple<int, char, std::string> xs{1, '2', std::string{"345"}};
    static_assert(std::tuple_size<decltype(xs)>::value == 3u, "");

    Поскольку размер кортежа закодирован в его типе, он всегда доступен во время компиляции независимо от того, является ли кортеж constexpr или нет. В Хане это реализуется посредством возврата длины Интегрального Постоянства . Поскольку значение IntegralConstant закодировано в его типе, результат длины содержится в типе объекта, который он возвращает, и поэтому длина известна во время компиляции. Поскольку длина переходит от значения времени выполнения (контейнера) к значению времени компиляции (IntegralConstant), длина является тривиальным примером алгоритма перекрестной фазы (тривиального, потому что он на самом деле не манипулирует кортежем). Другой алгоритм, очень похожий на длина, это алгоритм is_empty, который возвращает, пуст ли контейнер:

    // ^^^^^^^^^^^^^^^^^^^^^^^ assertion done at compile-time

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

    bool any_garfield = hana::any_of(animals, [](auto animal) {
    return animal.name == "Garfield"s;
    });

    В этом примере результат не может быть известен во время компиляции, потому что предикат возвращает bool Это результат сравнения двух std::strings. Поскольку std::strings нельзя сравнивать во время компиляции, предикат должен работать во время выполнения, и общий результат алгоритма может быть известен только во время выполнения. Допустим, вместо этого мы использовали any_of со следующим предикатом:

    auto any_cat = hana::any_of(animals, [](auto x) {
    return std::is_same<decltype(x), Cat>{};
    });
    Note
    For this to work, the external adapters for std::integral_constant contained in <boost/hana/ext/std/integral_constant.hpp> must be included.

    Во-первых, поскольку предикат представляет собой только запрос информации о типе каждого элемента кортежа, то ясно, что его результат может быть известен во время компиляции. Поскольку количество элементов в наборе также известно во время компиляции, общий результат алгоритма теоретически может быть известен во время компиляции. Точнее, предикат возвращает значение инициализированного std::is_same<...>, которое наследуется от std::integral_constant. Хана распознает эти объекты, и алгоритм написан таким образом, что сохраняет время компиляции результат предиката. В конце концов, any_of возвращает IntegralConstant, удерживая результат алгоритма, и мы используем вычет типа компилятора умным способом, чтобы сделать его легким. Следовательно, это было бы эквивалентно написанию (но тогда вам нужно было бы уже знать результат алгоритма!):

    hana::integral_constant<bool, true> any_cat = hana::any_of(animals, [](auto x) {
    return std::is_same<decltype(x), Cat>{};
    });

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

    auto animals = hana::make_tuple(Fish{"Nemo"}, Cat{"Garfield"}, Dog{"Snoopy"});
    auto no_garfield = hana::filter(animals, [](auto animal) {
    return animal.name != "Garfield"s;
    });

    Очевидно, мы знаем, что предикат вернется ложным только на втором элементе, и, следовательно, результат должен быть [Рыба, Собака] кортеж. Однако компилятор не может знать этого, поскольку результат предиката является результатом вычисления времени выполнения, которое происходит после того, как компилятор закончил свою работу. Следовательно, у компилятора недостаточно информации для определения типа возврата алгоритма. Однако мы можем фильтр Та же последовательность с любым предикатом, результат которого доступен во время компиляции:

    auto mammals = hana::filter(animals, [](auto animal) {
    return hana::type_c<decltype(animal)> != hana::type_c<Fish>;
    });

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

    Это конец раздела об алгоритмах. Хотя это представляет собой довольно полное объяснение фазового взаимодействия внутри алгоритмов, более глубокое понимание можно получить, прочитав расширенный раздел на constexpr и ссылку на Constant и IntegralConstant.

    Warning
    Hana's algorithms are constexpr function objects instead of being template functions. This allows passing them to higher-order algorithms, which is very useful. However, since those function objects are defined at namespace scope in the header files, this causes each translation unit to see a different algorithm object. Hence, the address of an algorithm function object is not guaranteed to be unique across translation units, which can cause an ODR violation if one relies on such an address. So, in short, do not rely on the uniqueness of the address of any global object provided by Hana, which does not make sense in the general case anyway because such objects are constexpr. See issue #76 for more information.

    Performance considerations


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

    Note
    The benchmarks presented in this section are updated automatically when we push to the repository. If you notice results that do not withstand the claims made here, open a GitHub issue; it could be a performance regression.
    Warning
    As of writing this, not all of Hana's containers are optimized. Implementing Hana was a big enough challenge that containers were initially written naively and are now in the process of being rigorously optimized. In particular, the associative containers (hana::map and hana::set) have a pretty bad compile-time behavior because of their naive implementation, and their runtime behavior also seems to be problematic in some cases. Improving this situation is in the TODO list.

    Compile-time performance

    Метапрограммирование на C++ приносит свою долю ужасных вещей. Одной из наиболее раздражающих и известных проблем, связанных с ним, является бесконечное время компиляции. Хана утверждает, что она более эффективна в компиляции, чем ее предшественники; это смелое утверждение, и теперь мы постараемся его поддержать. Конечно, Хана не может творить чудеса; метапрограммирование является побочным продуктом системы шаблонов C++, и компилятор не предназначен для использования в качестве интерпретатора для некоторых метаязыков. Однако, используя передовые и тщательно проверенные методы, Hana может минимизировать нагрузку на компилятор.

    Note
    While Hana has better compile-times than pre-C++11 metaprogramming libraries, modern libraries supporting only type-level computations (such as Brigand) can provide better compile-times, at the cost of generality. Indeed, Hana's ability to manipulate runtime values comes at a compile-time cost, no matter how hard we try to mitigate it. If you want to use Hana for intensive type-level computations, you should benchmark and see whether it suits you.

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

    Во-первых, Hana минимизирует свою зависимость от препроцессора. В дополнение к получению более чистых сообщений об ошибках во многих случаях это сокращает общее время анализа и предварительной обработки файлов заголовка. Кроме того, поскольку Hana поддерживает только передовые компиляторы, в библиотеке очень мало обходных путей, что приводит к более чистой и маленькой библиотеке. Наконец, Хана сводит к минимуму зависимость от любых внешних зависимостей. В частности, он использует только другие библиотеки Boost в нескольких конкретных случаях, и он не полагается на стандартную библиотеку для большей части. Для этого есть несколько причин (кроме включения времени); они задокументированы в rationales.

    Ниже приведен график, показывающий время, необходимое для включения различных библиотек. График показывает время для включения всего в (внешний) публичный API каждой библиотеки. Например, для Hana это означает заголовок <boost/hana.hpp>, который исключает внешние адаптеры. Другие библиотеки, такие как Boost. Это означает включение всех публичных заголовков в каталог boost/fusion/, но не адаптеров для внешних библиотек, таких как MPL.

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

    Note
    You can zoom on the chart by selecting an area to zoom into. Also, you can hide a series of points by clicking on it in the legend on the right.

    Методология бенчмарка заключается в том, чтобы всегда создавать последовательности наиболее эффективным способом. Для Ханы и std::tuple это просто означает использование соответствующей функции make_tuple. Однако для MPL это означает создание mpl::vectorN размера до 20, а затем использование mpl::push_back для создания более крупных векторов. Мы используем аналогичную технику для последовательностей Fusion. Причина этого заключается в том, что последовательности Fusion и MPL имеют фиксированные ограничения по размеру, и методы, используемые здесь, оказались самым быстрым способом создания более длинных последовательностей.

    Для полноты мы также представляем стоимость компиляции времени создания std::array с элементами n. Однако обратите внимание, что std::array Они могут содержать только элементы одного типа, поэтому мы сравниваем яблоки и апельсины. Как вы можете видеть, стоимость создания std::array постоянна и по существу не существует (ненулевые накладные расходы - это просто включение заголовка ). Следовательно, в то время как Hana обеспечивает улучшенное время компиляции по сравнению с другими гетерогенными контейнерами, пожалуйста, придерживайтесь обычных однородных контейнеров, если это все, что вам нужно для вашего приложения.

    Вы также можете увидеть, что создание последовательностей имеет незначительную стоимость. На самом деле, это действительно самая дорогая часть выполнения гетерогенных вычислений, как вы увидите на следующих диаграммах. Поэтому, когда вы смотрите на диаграммы ниже, имейте в виду стоимость простого создания последовательностей. Также отметим, что здесь будут представлены только самые важные алгоритмы, но проект Metabench предоставляет микро бенчмарки для производительности времени компиляции практически для всех алгоритмов Ханы. Кроме того, в тестах мы сравниваем несколько различных библиотек. Однако, поскольку Hana и Fusion могут работать со значениями, а не только с типами, сравнение их алгоритмов с библиотеками типа MPL не совсем справедливо. Действительно, алгоритмы Hana и Fusion являются более мощными, поскольку они также позволяют выполнять эффекты времени выполнения. Однако сравнение между Fusion и Hana справедливо, потому что обе библиотеки столь же мощны. Наконец, мы не можем показать бенчмарки алгоритмов для std::tuple, поскольку стандарт не предоставляет эквивалентных алгоритмов. Конечно, мы могли бы использовать внешние адаптеры Ханы, но это не было бы верным сравнением.

    Первый алгоритм, который является повсеместным в метапрограммировании, это преобразование . Он берет последовательность и функцию и возвращает новую последовательность, содержащую результат применения функции к каждому элементу. На следующей диаграмме представлена производительность компиляции при применении преобразования к последовательности элементов n. Ось x представляет количество элементов в последовательности, а ось y представляет время компиляции в секундах. Также обратите внимание, что мы используем эквивалент transform в каждой библиотеке; мы не используем эквивалент Ханы transform через подъём. Например, адаптеры Fusion, потому что мы действительно хотим сравнить их реализацию с нашей.

    Здесь мы видим, что кортеж Ханы работает лучше, чем все другие альтернативы. В основном это связано с тем, что мы используем расширение пакета вариадных параметров C++11 для реализации этого алгоритма под капотом, что довольно эффективно.

    Прежде чем двигаться дальше, важно упомянуть кое-что о методологии бенчмарка для алгоритмов Fusion. Некоторые алгоритмы в Fusion являются ленивыми, что означает, что они на самом деле ничего не выполняют, а просто возвращают измененное представление к исходным данным. Это случай fusion::transform, который просто возвращает преобразованный вид, который применяет функцию к каждому элементу исходной последовательности по мере доступа к этим элементам. Если мы хотим вообще что-то сопоставить, нам нужно навязать оценку этой точки зрения, как это в конечном итоге произойдет при доступе к элементам последовательности в реальном коде. Однако для сложных вычислений с несколькими слоями ленивый подход может дать существенно другой профиль времени компиляции. Конечно, эта разница плохо представлена в микро бенчмарках, поэтому имейте в виду, что эти бенчмарки дают только часть общей картины. Для полноты в остальной части раздела мы упомянем, когда алгоритм Fusion ленив, чтобы вы знали, когда мы искусственно заставляем оценку алгоритма с целью бенчмаркинга.

    Note
    We are currently considering adding lazy views to Hana. If this feature is important to you, please let us know by commenting this issue.

    Второй важный класс алгоритмов — складки. Складки могут быть использованы для реализации многих других алгоритмов, таких как count_if, minimum и так далее. Следовательно, хорошая производительность времени компиляции для алгоритмов сгиба обеспечивает хорошую производительность времени компиляции для этих производных алгоритмов, поэтому мы представляем здесь только сгибы. Также обратите внимание, что все немонадические варианты сгибов несколько эквивалентны по времени компиляции, поэтому мы представляем только левые сгибы. На следующей диаграмме представлена производительность компиляции при применении fold_left к последовательности элементов n. Ось x представляет количество элементов в последовательности, а ось y представляет время компиляции в секундах. Функция, используемая для складывания, является фиктивной функцией, которая ничего не делает. В реальном коде вы, вероятно, сложите нетривиальную операцию, поэтому кривые будут хуже, чем это. Однако это микро бенчмарки и, следовательно, они показывают только производительность самого алгоритма.

    Третий и последний алгоритм, представленный здесь, является алгоритмом find_if. Этот алгоритм трудно реализовать эффективно, поскольку он требует остановки на первом элементе, который удовлетворяет заданному предикату. По той же причине современные методы не очень помогают нам здесь, поэтому этот алгоритм представляет собой хорошую проверку качества реализации Hana, без учета бесплатного обеда, предоставленного для использования C++14.

    Как вы можете видеть, Хана работает лучше, чем Fusion, а также MPL, но Хана find_if Можно использовать и с значениями, в отличие от MPL. На этом завершается раздел о производительности компиляции. Если вы хотите увидеть производительность алгоритма, который мы не представили здесь, проект Metabench предоставляет тесты времени компиляции для большинства алгоритмов Ханы.

    Runtime performance

    Хана была разработана, чтобы быть очень эффективной во время выполнения. Но прежде чем мы углубимся в детали, давайте проясним одну вещь. Хана, будучи библиотекой метапрограммирования, которая позволяет манипулировать как типами, так и значениями, не всегда имеет смысл даже говорить о производительности во время выполнения. Действительно, для вычислений на уровне типов и вычислений на IntegralConstant производительность среды выполнения просто не является проблемой, потому что результат вычислений содержится в type, который является чисто компиляционным объектом времени. Другими словами, эти вычисления включают в себя только работу в режиме компиляции, и никакой код даже не генерируется для выполнения этих вычислений во время выполнения. Единственным случаем, когда имеет смысл обсуждать производительность среды выполнения, является манипулирование значениями времени выполнения в гетерогенных контейнерах и алгоритмах, потому что это единственный случай, когда компилятор должен генерировать некоторый код времени выполнения. Поэтому в оставшейся части этого раздела мы будем изучать только вычисления такого рода.

    Как и в случае с контрольными показателями времени компиляции, методология, используемая для измерения производительности во время выполнения в Хане, ориентирована на данные, а не на аналитические данные. Другими словами, вместо того, чтобы пытаться определить сложность алгоритма, подсчитав количество базовых операций, которые он выполняет в зависимости от размера входа, мы просто проводим измерения для самых интересных случаев и видим, как он ведет себя. Для этого есть несколько причин. Во-первых, мы не ожидаем, что алгоритмы Ханы будут вызываться на большие входы, поскольку эти алгоритмы работают на гетерогенных последовательностях, длина которых должна быть известна во время компиляции. Например, если вы попытаетесь вызвать алгоритм find_if на последовательности 100k элементов, ваш компилятор просто умрет, пытаясь сгенерировать код для этого алгоритма. Следовательно, алгоритмы не могут быть вызваны на очень большие входы, и аналитический подход теряет свою привлекательность. Во-вторых, процессоры превратились в довольно сложных зверей, и фактическая производительность, которую вы сможете выжать, на самом деле контролируется гораздо большим количеством шагов, чем делает ваш алгоритм. Например, плохое поведение кэша или неправильное прогнозирование ветвей могут превратить теоретически эффективный алгоритм в медленный, особенно для небольших входов. Поскольку Хана вызывает много разворотов, эти факторы должны быть рассмотрены еще более тщательно, и любой аналитический подход, вероятно, только утешит нас. Вместо этого нам нужны жесткие данные и красивые диаграммы для их отображения!

    Note
    Like for compile-time performance, we're forcing the evaluation of some Fusion algorithms that are normally lazy. Again, depending on the complexity of the computation, a lazy algorithm may cause substantially different code to be generated or a different design to be used, for better or worse. Keep this in mind when you look at these runtime benchmarks. If performance is absolutely critical to your application, you should profile before and after switching from Fusion to Hana. And let us know if Hana performs worse; we'll fix it!

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

    Так же, как мы изучили только пару ключевых алгоритмов для производительности компиляции времени, мы сосредоточимся на производительности выполнения нескольких алгоритмов. Для каждого аспекта мы сравним алгоритм, реализованный различными библиотеками. Наша цель - всегда быть по крайней мере столь же эффективным, как Boost. Fusion, который близок к оптимальности с точки зрения производительности во время выполнения. Для сравнения, мы также показываем тот же алгоритм, который выполняется на последовательности времени выполнения, и на последовательности, длина которой известна во время компиляции, но чей алгоритм преобразования не использует явную прокрутку петли. Все тесты, представленные здесь, выполнены в Release CMake конфигурация, которая заботится о прохождении соответствующих флагов оптимизации (обычно -O3). Начнем со следующей диаграммы, которая показывает время выполнения, необходимое для преобразования различных типов последовательностей:

    Note
    Keep in mind that fusion::transform is usually lazy, and we're forcing its evaluation for the purpose of benchmarking.

    Как видите, Hana и Fusion находятся на одной линии. std::array немного медленнее для больших наборов данных коллекций, а std::vector заметно медленнее для больших коллекций. Поскольку мы также хотим обратить внимание на вздутие кода, давайте посмотрим на размер исполняемого файла, созданного для того же сценария:

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

    Здесь вы можете видеть, что все работают почти одинаково, что является хорошим признаком того, что Хана, по крайней мере, не портит дела. Опять же, давайте посмотрим на исполняемый размер:

    Здесь снова размер кода не взорвался. Так что, по крайней мере, для умеренного использования Ханы (и Fusion, поскольку у них одна и та же проблема), вздутие кода не должно быть серьезной проблемой. Контейнеры на графиках, которые мы только что представили, содержат случайно сгенерированные ints, которые дешево копировать и хорошо поддаются микро бенчмаркам. Однако что происходит, когда мы цепляем несколько алгоритмов на контейнере, элементы которого дорого копировать? В более общем плане вопрос заключается в следующем: когда алгоритм передается временному объекту, он использует возможность избежать ненужных копий? Подумайте:

    auto xs = hana::make_tuple("some"s, "huge"s, "string"s);
    // No copy of xs's elements should be made: they should only be moved around.
    auto ys = hana::reverse(std::move(xs));

    Чтобы ответить на этот вопрос, мы рассмотрим диаграмму, созданную при сравнении вышеупомянутого кода для строк около 1k символов. Однако обратите внимание, что это не имеет смысла для стандартных библиотечных алгоритмов, потому что они не возвращают контейнеры.

    Note
    Keep in mind that fusion::reverse is usually lazy, and we're forcing its evaluation for the purpose of benchmarking.

    Как видите, Хана быстрее Fusion, вероятно, из-за более последовательного использования семантики движения в реализации. Если бы мы не предоставили временный контейнер для обратного , Хана не могла бы выполнить никакого перемещения, и обе библиотеки выполнили бы аналогично:

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

    Integration with external libraries


    Hana обеспечивает интеграцию с некоторыми существующими библиотеками. В частности, это означает, что вы можете использовать некоторые контейнеры из этих библиотек в алгоритмах Ханы, просто включив соответствующий заголовок, делающий мост между Ханой и внешним компонентом. Это может быть очень полезно для переноса существующего кода, например. Fusion/MPL to Hana:

    // In the old code, this used to receive a Fusion sequence.
    // Now, it can be either a Hana sequence or a Fusion sequence.
    template <typename Sequence>
    void f(Sequence const& seq) {
    hana::for_each(seq, [](auto const& element) {
    std::cout << element << std::endl;
    });
    }
    Note
    At this time, only adapters to use data types from other libraries inside Hana are provided; adapters for the other way around (using Hana containers inside other libraries) are not provided.

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

    auto r = std::ratio<3, 4>{} + std::ratio<4, 5>{}; // error, the operator is not defined!

    Вместо этого вы должны использовать следующее:

    #include <ratio>
    namespace hana = boost::hana;

    Но иногда все гораздо хуже. Некоторые внешние компоненты определяют операторов, но они не обязательно имеют ту же семантику, что и у Ханы. Например, сравнение двух std::tuples разной длины даст ошибку при использовании оператора==:

    std::make_tuple(1, 2, 3) == std::make_tuple(1, 2); // compiler error

    С другой стороны, сравнение связок Ханы разной длины просто вернет ложный IntegralConstant:

    hana::make_tuple(1, 2, 3) == hana::make_tuple(1, 2); // hana::false_c

    Это связано с тем, что std::tuple определяет собственных операторов, а их семантика отличается от семантики операторов Ханы. Решение состоит в том, чтобы придерживаться названных функций Ханы вместо использования операторов, когда вы знаете, что вам придется работать с другими библиотеками:

    hana::equal(std::make_tuple(1, 2, 3), std::make_tuple(1, 2)); // hana::false_c

    При использовании внешних адаптеров также следует быть осторожным, чтобы не забыть включить правильные заголовки моста. Например, предположим, что я хочу использовать буст. Вектор MPL с Ханой. Включаю соответствующий заголовок моста:

    #include <boost/hana/ext/boost/mpl/vector.hpp> // bridge header
    using Vector = mpl::vector<int, char, float>;
    static_assert(hana::front(Vector{}) == hana::type_c<int>, "");
    Note
    The exact layout of these bridge headers is documented in the section about Header organization.

    Теперь, однако, предположим, что я использую mpl::size для запроса размера вектора, а затем сравниваю его с некоторым значением. Я также мог бы использовать hana::length и все было бы хорошо, но потерпите меня ради примера:

    using Size = mpl::size<Vector>::type;
    static_assert(hana::equal(Size{}, hana::int_c<3>), ""); // breaks!

    Причина, по которой это нарушается, заключается в том, что mpl::size возвращает MPL IntegralConstant, и Хана не может знать об этом, если вы не включите соответствующий заголовок моста. Вместо этого вы должны сделать следующее:

    using Size = mpl::size<Vector>::type;
    static_assert(hana::equal(Size{}, hana::int_c<3>), "");

    Мораль в том, что при работе с внешними библиотеками нужно быть немного осторожным в том, какими объектами вы манипулируете. Окончательный подводный камень — ограничения реализации во внешних библиотеках. Многие старые библиотеки имеют ограничения относительно максимального размера разнородных контейнеров, которые могут быть созданы с ними. Например, в нем нельзя создать список Fusion из более чем элементов FUSION_MAX_LIST_SIZE. Очевидно, что эти пределы унаследованы Ханой и, например, попытка вычислить перестановки fusion::list, содержащие 5 элементов (в результате список будет содержать 120 элементов), потерпит неудачу

    auto list = fusion::make_list(1, 2, 3, 4, 5);
    auto oh_jeez = hana::permutations(list); // probably won't make it

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

    Hana's core


    Цель этого раздела — дать обзор ядра Ханы на высоком уровне. Это ядро основано на понятии tag, которое заимствовано из Boost. Фьюжн и буст. Библиотеки MPL, но Хана продвинулась гораздо дальше. Эти теги затем используются для нескольких целей, таких как настройка алгоритма, группировка документации, улучшение сообщений об ошибках и преобразование контейнеров в другие контейнеры. Благодаря модульному дизайну, Хана может быть расширена очень легко. Фактически, вся функциональность библиотеки обеспечивается с помощью специального механизма настройки, который объясняется здесь.

    Tags

    Гетерогенное программирование - это программирование с объектами, имеющими разные типы. Однако очевидно, что некоторые семейства объектов, хотя и имеют разные представления (типы C++), сильно связаны между собой. Например, типы std::integral_constant различны для каждого различного n, но концептуально все они представляют одну и ту же вещь; число времени компиляции. Тот факт, что std::integral_constant{} и std::integral_constant{} имеют разные типы, является лишь побочным эффектом того факта, что мы используем их тип для кодирования значения этих объектов. Действительно, при манипулировании последовательностью std::integral_constants, скорее всего, вы действительно думаете о ней как о однородной последовательности воображаемого типа integral_constant, игнорируя фактические типы объектов и делая вид, что все они просто integral_constants с различными значениями.

    Чтобы отразить эту реальность, Hana предоставляет теги, представляющие его гетерогенные контейнеры и другие объекты времени компиляции. Например, все integral_constant имеют разные типы, но все они имеют один и тот же тег integral_constant_tag. Это позволяет программисту мыслить в терминах одного типа, а не пытаться мыслить в терминах реальных типов объектов. Конкретно теги реализуются как пустые structs. Чтобы выделить их, Хана принимает соглашение о названии этих тегов, добавив суффикс _tag.

    Note
    The tag of an object of type T can be obtained by using tag_of<T>::type, or equivalently tag_of_t<T>.

    Теги являются расширением для обычных типов C++. Действительно, по умолчанию тег типа T сам по себе является T, и ядро библиотеки предназначено для работы в этих случаях. Например, hana::make ожидает либо тега, либо фактического типа; если вы отправите ему тип T, он сделает логическую вещь и создаст объект типа T с помощью аргументов, которые вы ему передадите. Однако, если вы передаете ему тег, вы должны специализироваться на сделать для этого тега и обеспечить свою собственную реализацию, как описано ниже. Поскольку теги являются расширением обычных типов, мы в конечном итоге в основном рассуждаем с точки зрения тегов вместо обычных типов, и в документации иногда используются слова type, data type и tag взаимозаменяемо.

    Tag dispatching

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

    template <typename ...T>
    void function(boost::fusion::vector<T...> v) {
    // whatever
    }

    Если ты знаешь Буст. Фьюжн, тогда вы, вероятно, знаете, что это не сработает. Это потому, что рост. Вектор слияния не обязательно является специализацией шаблона boost::fusion::vector. Вектор слияния также существует в пронумерованных формах, которые бывают разных типов:

    boost::fusion::vector1<T>
    boost::fusion::vector2<T, U>
    boost::fusion::vector3<T, U, V>
    ...

    Это деталь реализации, требуемая отсутствием вариадных шаблонов в C++03, которые просачиваются в интерфейс. Это печально, но нам нужен способ обойти это. Для этого мы используем инфраструктуру с тремя различными компонентами:

    1. Метафункция, связывающая один тег с каждым типом в семействе родственных типов. В Хане к этому тегу можно получить доступ с помощью метафункцииtag_of. В частности, для любого типаTtag_of<T>::typeиспользуется тег для его отправки.
    2. Функция, принадлежащая публичному интерфейсу библиотеки, для которой мы хотели бы иметь возможность обеспечить индивидуальную реализацию. В Хане эти функции являются алгоритмами, связанными с концепцией, такими какtransformилиunpack.
    3. Реализация функции, параметризованная с помощью тега(ов) аргумента(ов), переданного функции. В Хане это обычно делается с помощью отдельного шаблона под названиемxxx_impl(для функции интерфейсаxxx) с вложеннойapplyстатической функцией, как будет показано ниже.

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

    template <typename Tag>
    struct print_impl {
    template <typename X>
    static void apply(std::ostream&, X const&) {
    // possibly some default implementation
    }
    };
    template <typename X>
    void print(std::ostream& os, X x) {
    using Tag = typename hana::tag_of<X>::type;
    }

    Теперь давайте определим тип, который нуждается в отправке тегов для настройки поведения print. Хотя некоторые примеры C++14 существуют, они слишком сложны для отображения в этом руководстве, и поэтому мы будем использовать набор C++03, реализованный в виде нескольких различных типов, чтобы проиллюстрировать технику:

    struct vector_tag;
    struct vector0 {
    using hana_tag = vector_tag;
    static constexpr std::size_t size = 0;
    };
    template <typename T1>
    struct vector1 {
    T1 t1;
    using hana_tag = vector_tag;
    static constexpr std::size_t size = 1;
    template <typename Index>
    auto const& operator[](Index i) const {
    static_assert(i == 0u, "index out of bounds");
    return t1;
    }
    };
    template <typename T1, typename T2>
    struct vector2 {
    T1 t1; T2 t2;
    using hana_tag = vector_tag;
    static constexpr std::size_t size = 2;
    // Using Hana as a backend to simplify the example.
    template <typename Index>
    auto const& operator[](Index i) const {
    return *hana::make_tuple(&t1, &t2)[i];
    }
    };
    // and so on...

    Вложенная часть , использующая hana_tag = vector_tag;, представляет собой простой способ управления результатом метафункции tag_of и, следовательно, тегом типа vectorN. Это объясняется ссылкой на tag_of. Наконец, если вы хотите настроить поведение функции print для всех типов vectorN, вам, как правило, придется написать что-то вдоль строк

    void print(std::ostream& os, vector0)
    { os << "[]"; }
    template <typename T1>
    void print(std::ostream& os, vector1<T1> v)
    { os << "[" << v.t1 << "]"; }
    template <typename T1, typename T2>
    void print(std::ostream& os, vector2<T1, T2> v)
    { os << "[" << v.t1 << ", " << v.t2 << "]"; }
    // and so on...

    Теперь, с помощью диспетчеризации тегов, вы можете полагаться на vectorNs и специализироваться только на print_impl Структура вместо:

    template <>
    struct print_impl<vector_tag> {
    template <typename vectorN>
    static void apply(std::ostream& os, vectorN xs) {
    constexpr auto N = hana::size_c<vectorN::size>;
    os << "[";
    N.times.with_index([&](auto i) {
    os << xs[i];
    if (i != N - hana::size_c<1>) os << ", ";
    });
    os << "]";
    }
    };

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

    template <typename X>
    void print(std::ostream& os, X x) {
    // **** check some precondition ****
    // The precondition only has to be checked here; implementations
    // can assume their arguments to always be sane.
    using Tag = typename hana::tag_of<X>::type;
    }
    Note
    Checking preconditions does not make much sense for a print function, but consider for example a function to get the nth element of a sequence; you might want to make sure that the index is not out-of-bounds.

    Этот метод также облегчает предоставление функций интерфейса в качестве функциональных объектов вместо обычных перегруженных функций, потому что только сама функция интерфейса должна пройти через трудности определения функционального объекта. Функциональные объекты имеют несколько преимуществ перед перегруженными функциями, такими как возможность использования в алгоритмах более высокого порядка или в качестве переменных:

    // Defining a function object is only needed once and implementations do not
    // have to worry about static initialization and other painful tricks.
    struct print_t {
    template <typename X>
    void operator()(std::ostream& os, X x) const {
    using Tag = typename hana::tag_of<X>::type;
    }
    };
    constexpr print_t print{};

    Как вы, наверное, знаете, возможность реализовать алгоритм для многих типов одновременно чрезвычайно полезна (это именно цель шаблонов C++!). Однако еще более полезной является возможность реализации алгоритма для многих типов , удовлетворяющих некоторому условию. В настоящее время в шаблонах C++ отсутствует возможность ограничения параметров шаблонов, но с целью решения этой проблемы внедряется языковая функция под названием concepts.

    Имея в виду что-то подобное, алгоритмы Ханы поддерживают дополнительный уровень отправки тегов к тому, что было объяснено выше. Этот слой позволяет «специализировать» алгоритм на все типы, удовлетворяющие некоторому предикату. Например, предположим, что мы хотели реализовать функцию print выше для всех типов, которые представляют собой некоторую последовательность. Прямо сейчас у нас не будет простого способа сделать это. Тем не менее, отправка тегов для алгоритмов Ханы настроена немного иначе, чем показано выше, и поэтому мы можем написать следующее:

    template <typename Tag>
    struct print_impl<Tag, hana::when<Tag represents some kind of sequence>> {
    template <typename Seq>
    static void apply(std::ostream& os, Seq xs) {
    // Some implementation for any sequence
    }
    };

    где Tag представляет некоторую последовательность , должно быть только булевое выражение, представляющее, является ли Tag последовательностью. Посмотрим, как можно создать такие предикаты в следующем разделе, но пока предположим, что он просто работает. Не вдаваясь в подробности того, как настроена эта диспетчеризация тегов, вышеупомянутая специализация будет подхвачена только тогда, когда предикат будет удовлетворен, и если не будет найдено лучшего соответствия. Следовательно, например, если бы наш vector_tag удовлетворял предикату, наша первоначальная реализация vector_tag все равно была бы предпочтительнее, чем специализация на основе hana:: when, поскольку она представляет собой лучшее соответствие. В целом, любая специализация (явная или частичная) не с использованием hana::когда будет предпочтительнее специализации с использованием hana::когда , которая была разработана, чтобы быть как можно более неожиданной с точки зрения пользователя. Это охватывает почти все, что можно сказать о диспетчеризации тегов в Хане. В следующем разделе будет объяснено, как мы можем создать концепции C++ для метапрограммирования, которые затем могут быть использованы в сочетании с hana:: When для достижения большой выразительности.

    Emulation of C++ concepts

    Реализация концепций в Хане очень проста. В основе концепции лежит шаблон struct с вложенным ::значение boolean, представляющий, является ли данный тип моделью концепта:

    template <typename T>
    struct Concept {
    static constexpr bool value = whether T models Concept;
    };

    Затем можно проверить, является ли тип T моделью Concept, посмотрев на Concept::value. Достаточно просто, да? Теперь, хотя способ реализации проверки не должен быть чем-то конкретным, остальная часть этого раздела объяснит, как это обычно делается в Хане, и как она взаимодействует с отправкой тегов. Тогда вы сможете определить свои собственные концепции, если захотите, или, по крайней мере, лучше понять, как Хана работает внутри.

    Обычно концепция, определенная Ханой, требует, чтобы любая модель реализовывала некоторые функции, распределенные по тегам. Например, Foldable Концепция требует, чтобы любая модель определяла по меньшей мере одну из hana::unpack и hana::fold_left. Конечно, понятия обычно также определяют семантические требования (называемые законами), которые должны удовлетворяться их моделями, но эти законы не проверяются концепцией. Но как проверить правильность выполнения некоторых функций? Для этого нам придется немного изменить способ определения методов, распределенных по тегам, как показано в предыдущем разделе. Давайте вернемся к нашему примеру print и попытаемся определить концепцию Printable для тех объектов, которые могут быть printed. Наша конечная цель - иметь шаблонную структуру, такую как

    template <typename T>
    struct Printable {
    static constexpr bool value = whether print_impl<tag of T> is defined;
    };

    Чтобы узнать, определен ли print_impl<...>, мы изменим print_impl Чтобы он наследовал от специального базового класса, когда он не переопределен, и мы просто проверим, является ли print_impl наследуется от этого базового класса:

    struct special_base_class { };
    template <typename T>
    struct print_impl : special_base_class {
    template <typename ...Args>
    static constexpr auto apply(Args&& ...) = delete;
    };
    template <typename T>
    struct Printable {
    using Tag = hana::tag_of_t<T>;
    static constexpr bool value = !std::is_base_of<special_base_class, print_impl<Tag>>::value;
    };

    Конечно, когда мы специализируемся на print_impl с пользовательским типом, мы не наследуем от этого special_base_class тип:

    struct Person { std::string name; };
    template <>
    struct print_impl<Person> /* don't inherit from special_base_class */ {
    // ... implementation ...
    };
    static_assert(Printable<Person>::value, "");
    static_assert(!Printable<void>::value, "");

    Как вы можете видеть, Printable::value действительно только проверяет, является ли print_impl Структура специализировалась на заказном типе. В частности, он даже не проверяет, определена ли вложенная функция ::apply или она синтаксически верна. Предполагается, что если специализироваться на print_impl для пользовательского типа, вложенная функция ::apply существует и является правильной. Если это не так, ошибка компиляции будет вызвана при попытке вызова print на объекте этого типа. Понятия в хане делают те же предположения.

    Поскольку эта модель наследования из специального базового класса довольно распространена в Хане, библиотека предоставляет фиктивный тип под названием hana::default_, который можно использовать вместо special_base_class. Тогда вместо использования std::is_base_of можно использовать hana::is_default, что выглядит приятнее. С этим синтаксическим сахаром код становится:

    template <typename T>
    struct print_impl : hana::default_ {
    template <typename ...Args>
    static constexpr auto apply(Args&& ...) = delete;
    };
    template <typename T>
    struct Printable {
    using Tag = hana::tag_of_t<T>;
    static constexpr bool value = !hana::is_default<print_impl<Tag>>::value;
    };

    Это все, что нужно знать о взаимодействии между функциями и концепциями, распределенными по тегам. Тем не менее, некоторые концепции в Хане не полагаются исключительно на определение конкретных функций, распределенных по тегам, чтобы определить, является ли тип моделью концепции. Это может произойти, когда понятие просто вводит семантические гарантии через законы и утонченные понятия, но никаких дополнительных синтаксических требований. Определение такого понятия может быть полезным по нескольким причинам. Во-первых, иногда случается так, что алгоритм может быть реализован более эффективно, если мы можем предположить некоторые семантические гарантии X или Y, поэтому мы можем создать концепцию для обеспечения этих гарантий. Во-вторых, иногда можно автоматически определить модели для нескольких понятий, когда у нас есть дополнительные семантические гарантии, что избавляет пользователя от проблем с определением этих моделей вручную. Например, это относится к концепции Sequence, которая в основном добавляет семантические гарантии к Iterable и Foldable и, в свою очередь, позволяет нам определить модели для множества концепций, начиная от Comparable до Monad.

    Для этих концепций обычно необходимо специализировать соответствующую структуру шаблона в boost::hana пространства имен, чтобы обеспечить модель для пользовательского типа. Это похоже на предоставление печати, в которой говорится, что семантические гарантии, требуемые концепцией, соблюдаются обычным типом. Концепции, требующие четкой специализации, задокументируют этот факт. Вот и все! Это все, что нужно знать о концепциях в Хане, которая завершает этот раздел о ядре Ханы.

    Header organization


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

    Conclusion


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

    Fair warning: functional programming ahead

    Программирование с неоднородными объектами по своей сути функционально; поскольку невозможно изменить тип объекта, вместо этого должен быть введен новый объект, который исключает мутацию. В отличие от предыдущих библиотек метапрограммирования, дизайн которых был смоделирован на STL, Хана использует функциональный стиль программирования, который является источником значительной части его выразительности. Однако в результате многие концепции, представленные в ссылке, будут незнакомы программистам C++ без знания функционального программирования. Ссылка пытается сделать эти понятия доступными, используя интуицию, когда это возможно, но имейте в виду, что самые высокие награды обычно являются плодом некоторых усилий.

    Это завершает учебную часть документации. Я надеюсь, что вам понравится использовать библиотеку, и, пожалуйста, подумайте о том, чтобы внести вклад , чтобы сделать его еще лучше!

    &ndash, Луи

    Using the reference


    Что касается большинства общих библиотек, алгоритмы в Хане документированы концепцией, к которой они принадлежат (Foldable, Iterable, Searchable, Sequence и т. д.). Различные контейнеры затем документируются на их собственной странице, и концепции, которые они моделируют, документируются там. Концепции, моделируемые каким-либо контейнером, определяют, какие алгоритмы могут быть использованы с таким контейнером.

    Более конкретно, структура ссылки (доступная в меню слева) выглядит следующим образом:

    • Ядро
      Документация для базового модуля, содержащая все необходимое для создания концепций, типов данных и связанных с ними утилит. Это актуально, если вам нужно расширить библиотеку, но в противном случае вы, вероятно, можете игнорировать это.
    • Концепции
      Документация по всем понятиям, предоставляемым библиотекой. Каждая концепция:
      • Документы, функции которых должны быть реализованы абсолютно для того, чтобы смоделировать эту концепцию. Набор функций, которые должны быть предоставлены, называетсяминимальным полным определением.
      • Документирует семантические ограничения, которым должна удовлетворять любая модель этой концепции. Эти ограничения обычно называются законами и выражаются полуформальным математическим языком. Конечно, эти законы не могут быть проверены автоматически, но вы все равно должны убедиться, что вы их удовлетворяете.
      • Документирует концепцию(ы), которую она уточняет, если таковая имеется. Иногда концепция достаточно сильна, чтобы обеспечить модель концепции, которую она уточняет, или, по крайней мере, реализацию для некоторых связанных с ней функций. В этом случае концепция будет документировать, какие функции утонченной концепции она предоставляет, и как она это делает. Также иногда возможно, что модель для рафинированного концепта уникальна, в этом случае она может предоставляться автоматически. Когда это произойдет, это будет задокументировано, но вам не нужно делать ничего особенного, чтобы получить эту модель.
    • Типы данных
      Документация для всех структур данных, предоставляемых библиотекой. Каждая структура данных документирует концепцию (ы), которую она моделирует, и как она это делает. Он также документирует методы, связанные с ним, но не с какой-либо концепцией, напримерmaybeдляoptional.
    • Функциональный
      Объекты функции общего назначения, которые обычно полезны в чисто функциональной обстановке. В настоящее время они не привязаны к какой-либо концепции или контейнеру.
    • Внешние адаптеры
      Документация для всех адаптеров для внешних библиотек. Эти адаптеры задокументированы, как если бы они были родными типами, предоставляемыми Ханой, но очевидно, что Хана обеспечивает только слой совместимости между ними и библиотекой.
    • Варианты конфигурации
      Макрос, который можно использовать для настройки глобального поведения библиотеки.
    • Утверждение
      Макрос для выполнения различных видов утверждений.
    • Алфавитный индекс
      Алфавитный индекс всего предоставленного в библиотеке.
    • Заголовки
      Список всех заголовков, предоставленных библиотекой.
    • Подробности
      Детали реализации; не ходите туда. Все, что не было задокументировано или задокументировано в этой группе, не гарантируется стабильностью.

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

    Function signatures

    Как вы увидите в ссылке, несколько функций предоставляют подписи, задокументированные на полуформальном математическом языке. Мы находимся в процессе документирования всех функций таким образом, но это может занять некоторое время. Используемая нотация является обычной математической нотацией для определения функций. В частности, функция Return f(Arg1, ..., ArgN); может быть определена эквивалентно с использованием математической нотации как

    \[ \mathtt{f} : \mathtt{Arg}_1 \times \dots \times \mathtt{Arg}_n \to \mathtt{Return} \]

    Однако вместо того, чтобы документировать фактический аргумент и типы функций возврата, эти подписи написаны с точки зрения аргумента и тегов возврата. Это делается из-за неоднородной настройки, где фактический тип объекта обычно довольно бессмыслен и не помогает рассуждать о том, что возвращается или принимается функцией. Например, вместо документирования функции equal для integral_constants как

    \[ \mathtt{equal} : \mathtt{integral\_constant<T, n>} \times \mathtt{integral\_constant<T, m>} \to \mathtt{integral\_constant<bool, n == m>} \]

    вместо этого документируется с использованием integral_constant_tag, который действует как «тип» всех integral_constants. Обратите внимание, что поскольку равно является частью Сравнимое Понятие, оно не фактически задокументировано для hana::integral_constant конкретно, но идея есть:

    \[ \mathtt{equal} : \mathtt{integral\_constant\_tag<T>} \times \mathtt{integral\_constant\_tag<T>} \to \mathtt{integral\_constant\_tag<bool>} \]

    Это ясно передает намерение, что сравнение двух integral_constant возвращает другой integral_constant, содержащий bool. В общем, эта абстракция фактического представления объектов позволяет нам рассуждать на высоком уровне о функциях, даже если их фактическое возвращение и типы аргументов неоднородны и не полезны. Наконец, большинство функций ожидают, что элементы контейнера будут иметь некоторые свойства. Например, это относится к алгоритму sort, который, очевидно, требует, чтобы элементы контейнера были Orderable. Обычно мы записываем подпись для непредикативной версии sort как

    \[ \mathtt{sort} : \mathtt{S} \to \mathtt{S} \\ \text{where S is a Sequence} \]

    Однако это не отражает требование о том, что содержимое S является Orderable. Чтобы выразить это, мы используем следующую запись:

    \[ \mathtt{sort} : \mathtt{S(T)} \to \mathtt{S(T)} \\ \text{where S is a Sequence and T is Orderable} \]

    Один из способов увидеть это - сделать вид, что S, тег последовательности, фактически параметризован тегом элементов последовательности, T. Мы также делаем вид, что все элементы имеют один и тот же тег T, что в целом не так. Теперь, заявляя, что T должен быть Orderable, мы выражаем тот факт, что элементы последовательности должны быть Orderable. Эта нотация используется в различных ароматизаторах для выражения различных видов требований. Например, алгоритм cartesian_product берет последовательность последовательностей и возвращает картезианский продукт этих последовательностей в виде последовательности последовательностей. Используя нашу нотацию, это можно легко передать:

    \[ \mathtt{cartesian\_product} : \mathtt{S(S(T))} \to \mathtt{S(S(T))} \\ \text{where S is a Sequence} \]

    Acknowledgements


    Я хотел бы поблагодарить следующих лиц и организации за вклад в Хану так или иначе:

    • Зак Лейн и Мэтт Калабрез за оригинальную идею использования синтаксиса вызова функции для выполнения вычислений на уровне типов, как представлено в их презентации BoostConслайды 1]слайды 2.
    • Джоэл Фальку (Joel Falcou) два года подряд наставлял меня во время работы над Ханой в рамках программы 162 Google Summer of Code 163, Найл Дуглас (Niall Douglas) был администратором GSoC для Boost и помогал мне попасть в программу, и, наконец, Google для их потрясающей программы GSoC.
    • Поднять Руководящий комитетдля разблокировки гранта для меня, чтобы работать над Ханой зимой 2015 года, в качестве продления предыдущего года GSoC.
    • Несколько участниковC++Nowи членовBoost рассылкидля проницательных разговоров, комментариев и вопросов о проекте.

    Glossary


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

    forwarded(x)

    Это означает, что объект перенаправляется оптимально. Это означает, что если x является параметром, то это std::forwarded, а если это захваченная переменная, то она перемещается от того момента, когда заключенная лямбда представляет собой значение r.

    Также обратите внимание, что когда x может быть перемещено из, утверждение переадресовано (x); в функции с деклатипом (auto) не означает, что будет возвращена ссылка на rзначение x, что создаст ссылку на болтание. Скорее, это означает, что x возвращается значением, причем значение строится с помощью std::forwarded x.

    perfect-capture

    Это используется в лямбдах для обозначения того, что захваченные переменные инициализируются с использованием идеальной пересылки, как если бы использовался [x(forwarded(x))...]() { }.

    tag-dispatched

    Это означает, что документированная функция использует отправку tag, и, следовательно, точная реализация зависит от модели концепции, связанной с функцией.

    implementation-defined

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

    Rationales/FAQ


    Этот раздел документирует обоснование некоторых вариантов дизайна. Он также служит в качестве часто задаваемых вопросов (не так часто). Если вы считаете, что что-то должно быть добавлено в этот список, откройте вопрос GitHub, и мы рассмотрим возможность улучшения документации или добавления вопроса здесь.

    Why restrict usage of external dependencies?

    Для этого есть несколько причин. Сначала Хана — очень фундаментальная библиотека; мы в основном реализуем основной язык и стандартную библиотеку с поддержкой разнородных типов. Просматривая код, быстро понимаешь, что другие библиотеки редко нужны, и что почти все должно быть реализовано с нуля. Кроме того, поскольку Хана очень важна, есть еще больше стимулов для сохранения минимальных зависимостей, потому что эти зависимости будут переданы пользователям. Что касается минимальной зависимости от Boost, одним из основных аргументов в пользу его использования является переносимость. Однако, как передовая библиотека, Хана нацелена только на новейшие компиляторы. Следовательно, мы можем позволить себе зависеть от современных конструкций, и портативность, предоставленная нам с помощью Boost, будет в основном представлять мертвый вес.

    Why no iterators?

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

    Why leave some container's representation implementation-defined?

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

    auto tuple = hana::make_tuple(1, 'x', 3.4f);
    auto result = hana::find_if(tuple, [](auto const& x) {
    return hana::traits::is_integral(hana::typeid_(x));
    });

    Если предикат удовлетворяется для некоторого элемента кортежа, результат будет равен just(x). В противном случае результат будет равен ничего . Тем не менее, ничтонедоступность результата известна во время компиляции, что требует just(x) и ничто иметь разные типы. Теперь, скажем, вы хотели явно написать тип результата:

    some_type result = hana::find_if(tuple, [](auto const& x) {
    return hana::traits::is_integral(hana::typeid_(x));
    });

    Для того, чтобы обладать знаниями о том, что такое some_type, необходимо фактически выполнить алгоритм, поскольку some_type зависит от того, удовлетворяет предикат какой-либо элемент в контейнере или нет. Другими словами, если бы вы могли написать вышесказанное, то вы бы уже знали, каков результат алгоритма, и вам не нужно было бы выполнять алгоритм в первую очередь. В Boost.Fusion эта проблема решается с помощью отдельного result_of пространства имен, которое содержит метафункцию вычисления типа результата любого алгоритма с учетом типов переданных ему аргументов. Например, приведенный выше пример можно переписать с помощью Fusion следующим образом:

    using Container = fusion::result_of::make_vector<int, char, float>::type;
    Container tuple = fusion::make_vector(1, 'x', 3.4f);
    using Predicate = mpl::quote1<std::is_integral>;
    using Result = fusion::result_of::find_if<Container, Predicate>::type;
    Result result = fusion::find_if<Predicate>(tuple);

    Обратите внимание, что мы в основном выполняем вычисления дважды: один раз в результате_ пространства имен и один раз в нормальном слиянии пространства имен, что является весьма избыточным. До появления auto и decltype такие методы были необходимы для выполнения гетерогенных вычислений. Однако с появлением современного C++ потребность в явных типах возврата в контексте гетерогенного программирования в значительной степени устарела, и знание фактического типа контейнеров обычно не так полезно.

    Why Hana?

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

    Why define our own tuple?

    Поскольку Хана определяет множество алгоритмов на кортежах, можно было бы просто использовать std::tuple и предоставлять только алгоритмы, вместо того, чтобы также предоставлять наш собственный кортеж. Причина предоставления нашего собственного кортежа в основном производительность. Действительно, все протестированные реализации std::tuple имеют очень плохую производительность компиляции. Кроме того, чтобы получить действительно удивительную производительность компиляции, мы должны использовать внутреннее представление кортежа в некоторых алгоритмах, что требует определения нашего собственного. Наконец, некоторый сахар, такой как оператор [], не может быть предоставлен, если мы используем std::tuple, поскольку этот оператор должен быть определен как функция члена.

    How are names chosen?

    При выборе имени X я стараюсь сбалансировать следующие вещи (без определенного порядка):

    • Насколько идиоматиченXв C++?
    • Насколько идиоматиченXв остальном мире программирования?
    • Как хорошо имяXна самом деле, независимо от исторических причин
    • Как я, как автор библиотеки, отношусь кX
    • Что чувствуют пользователи библиотекиX
    • Есть ли технические причины не использоватьX, такие как столкновения имен или имена, зарезервированные по стандарту

    Конечно, хорошие имена всегда были и будут трудными. Имена всегда были и будут запятнаны предвзятостью самого автора. Тем не менее, я стараюсь выбирать имена разумным образом.

    How is the parameter order decided?

    В отличие от именования, которое является довольно субъективным, порядок параметров функции обычно довольно прост в определении. В принципе, правило большого пальца: «контейнер идет первым». Это всегда было так в Fusion и MPL, и это интуитивно понятно для большинства программистов на C++. Кроме того, в алгоритмах более высокого порядка я пытаюсь поставить параметр функции последним, чтобы многолинейные лямбда выглядели красиво:

    algorithm(container, [](auto x) {
    return ...;
    });
    // is nicer than
    algorithm([](auto x) {
    return ...;
    }, container);

    Why tag dispatching?

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

    Теперь диспетчеризация тегов была выбрана по сравнению с другими методами с двумя слоями по нескольким причинам. Во-первых, необходимость четко указать, как какой-либо тег является моделью концепции, возлагает на пользователя ответственность за соблюдение семантических требований концепции. Во-вторых, проверяя, является ли тип моделью какого-либо понятия, мы в основном проверяем, реализованы ли некоторые ключевые функции. В частности, мы проверяем, реализованы ли функции из минимального полного определения этой концепции. Например, Iterable проверяет, реализованы ли функции is_empty, at и drop_front для T. Тем не менее, единственный способ обнаружить это без отправки тегов - это проверить, действительны ли следующие выражения в контексте SFINAE:

    implementation_of_at(std::declval<T>(), std::declval<N>())
    implementation_of_is_empty(std::declval<T>())
    implementation_of_drop_front(std::declval<T>())

    К сожалению, это требует выполнения алгоритмов, которые могут либо вызвать ошибку времени компиляции, либо повредить производительность компиляции. Кроме того, это требует выбора произвольного индекса N для вызова at с: что, если Iterable пуст? При отправке тегов мы можем просто спросить, определены ли at_impl, is_empty_impl и drop_front_impl, и ничего не произойдет, пока мы фактически не назовем их вложенную функцию ::apply.

    Why not provide zip_longest?

    Для этого потребуется либо (1) наложение самых коротких последовательностей произвольным объектом, либо (2) наложение самых коротких последовательностей на объект, предоставленный пользователем при вызове zip_longest. Поскольку нет требования, чтобы все застегнутые последовательности имели элементы аналогичных типов, во всех случаях нет возможности обеспечить единый последовательный объект прокладки. Должен быть предоставлен набор предметов для прокладки, но я считаю, что это, возможно, слишком сложно, чтобы стоить этого на данный момент. Если вам нужна эта функция, откройте проблему GitHub.

    Why aren't concepts constexpr functions?

    Поскольку предложение концепции C++ отображает концепции в булевые функции constexpr, было бы разумно, чтобы Хана определяла свои концепции как таковые, а не как структуры с вложенным ::значение . Действительно, это был первый выбор, но его пришлось пересмотреть, потому что функции шаблонов имеют одно ограничение, которое делает их менее гибкими. В частности, функция шаблона не может быть передана метафункции более высокого порядка. Другими словами, невозможно написать следующее:

    template <??? Concept>
    struct some_metafunction {
    // ...
    };

    Этот тип кода очень полезен в некоторых контекстах, таких как проверка того, имеют ли два типа общую концепцию встраивания:

    template <??? Concept, typename T, typename U>
    struct have_common_embedding {
    // whether T and U both model Concept, and share a common type that also models Concept
    };

    С понятиями как функции boolean constexpr это не может быть записано в общем виде. Однако, когда концепции являются только структурами шаблонов, мы можем использовать параметры шаблонов шаблонов:

    template <template <typename ...> class Concept, typename T, typename U>
    struct have_common_embedding {
    // whether T and U both model Concept, and share a common type that also models Concept
    };

    Appendix I: Advanced constexpr


    В C++ граница между временем компиляции и временем выполнения туманна, что еще более верно с введением обобщенных постоянных выражений в C++14. Однако возможность манипулировать неоднородными объектами заключается в понимании этой границы, а затем пересечении ее по своему желанию. Цель этого раздела состоит в том, чтобы установить вещи прямо с constexpr; чтобы понять, какие проблемы он может решить, а какие нет. Этот раздел охватывает передовые концепции относительно постоянных выражений; только читатели с хорошим пониманием constexpr должны попытаться прочитать это.

    Constexpr stripping

    Начнем со сложного вопроса. Должен ли компилироваться следующий код?

    template <typename T>
    void f(T t) {
    static_assert(t == 1, "");
    }
    constexpr int one = 1;
    f(one);

    Ответ - нет, и ошибка, данная Клэнгом, выглядит так:

    error: static_assert expression is not an integral constant expression
    static_assert(t == 1, "");
    ^~~~~~

    Объяснение заключается в том, что внутри тела f t не является постоянным выражением, и, следовательно, его нельзя использовать в качестве операнда для static_assert. Причина в том, что такая функция просто не может быть создана компилятором. Чтобы понять проблему, рассмотрим, что должно произойти, когда мы инстанцируем f шаблон с конкретным типом:

    // Here, the compiler should generate the code for f<int> and store the
    // address of that code into fptr.
    void (*fptr)(int) = f<int>;

    Очевидно, что компилятор не может генерировать код f, который должен запускать static_assert, если t!= 1, потому что мы еще не указали t. Хуже того, генерируемая функция должна работать как на постоянных, так и на непостоянных выражениях:

    void (*fptr)(int) = f<int>; // assume this was possible
    int i = ...; // user input
    fptr(i);

    Очевидно, что код fptr не может быть сгенерирован, потому что для этого потребуется возможность static_assert на значении времени выполнения, что не имеет смысла. Кроме того, обратите внимание, что не имеет значения, выполняете ли вы функцию constexpr или нет; создание f constexpr будет только утверждать, что результат f является постоянным выражением, когда его аргумент является постоянным выражением, но это все еще не дает вам возможность узнать, были ли вы вызваны с постоянным выражением из тела f. Другими словами, то, что мы хотели бы, это что-то вроде:

    template <typename T>
    void f(constexpr T t) {
    static_assert(t == 1, "");
    }
    constexpr int one = 1;
    f(one);

    В этом гипотетическом сценарии компилятор будет знать, что t является постоянным выражением из тела f, а static_assert. Их можно заставить работать. Тем не менее, параметры constexpr не существуют в текущем языке, и их добавление вызовет очень сложные проблемы проектирования и реализации. Вывод этого небольшого эксперимента заключается в том, что аргумент, проходящий полоски прочь constexpr-ness. Что может быть неясным на данный момент, так это последствия этой раздевания, которые объясняются далее.

    Constexpr preservation

    Тот факт, что аргумент не является постоянным выражением, означает, что мы не можем использовать его в качестве нетипового шаблонного параметра, как связанный массив, внутри static_assert. Все, что требует постоянного выражения. Кроме того, это означает, что тип возврата функции не может зависеть от значения аргумента, который не является чем-то новым, если вы думаете об этом:

    template <int i>
    struct foo { };
    auto f(int i) -> foo<i>; // obviously won't work

    На самом деле, тип возврата функции может зависеть только от типов ее аргументов, и constexpr не может изменить этот факт. Это крайне важно для нас, потому что мы заинтересованы в манипулировании неоднородными объектами, что в конечном итоге означает возвращение объектов с различными типами в зависимости от аргумента функции. Например, функция может захотеть вернуть объект типа T в одном случае и объект типа U в другом; из нашего анализа мы теперь знаем, что эти «случаи» будут зависеть от информации, закодированной в типах аргументов, а не в их значениях .

    Чтобы сохранить constexpr-ness через прохождение аргумента, мы должны кодировать constexpr значение в тип, а затем передать необязательно-constexpr объект этого типа в функцию. Функция, которая должна быть шаблоном, может затем получить доступ к значению constexpr, закодированному внутри этого типа.

    Todo:
    Improve this explanation and talk about non-integral constant expressions wrapped into types.

    Side effects

    Позвольте задать сложный вопрос. Действителен ли следующий код?

    template <typename T>
    constexpr int f(T& n) { return 1; }
    int n = 0;
    constexpr int i = f(n);

    Ответ: да , но причина может быть неочевидна. Что происходит здесь, так это то, что у нас есть неconstexpr int n и функция constexpr f со ссылкой на свой аргумент. Причина, по которой большинство людей считают, что это не должно работать, заключается в том, что n не является constexpr. Однако мы ничего не делаем с n внутри f, поэтому нет никакой реальной причины, почему это не должно работать! Это немного похоже на throwing внутри функции constexpr:

    constexpr int sqrt(int i) {
    if (i < 0) throw "i should be non-negative";
    return ...;
    }
    constexpr int two = sqrt(4); // ok: did not attempt to throw
    constexpr int error = sqrt(-4); // error: can't throw in a constant expression

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

    template <typename T>
    constexpr int f(T& n, bool touch_n) {
    if (touch_n) n + 1;
    return 1;
    }
    int n = 0;
    constexpr int i = f(n, false); // ok
    constexpr int j = f(n, true); // error

    Ошибка, допущенная Клэнгом для второго вызова

    error: constexpr variable 'j' must be initialized by a constant expression
    constexpr int j = f(n, true); // error
    ^ ~~~~~~~~~~
    note: read of non-const variable 'n' is not allowed in a constant expression
    if (touch_n) n + 1;
    ^

    Теперь давайте немного поднимем игру и рассмотрим более тонкий пример. Действителен ли следующий код?

    template <typename T>
    constexpr int f(T n) { return 1; }
    int n = 0;
    constexpr int i = f(n);

    Единственная разница с нашим исходным сценарием заключается в том, что f теперь принимает свой аргумент по значению, а не по ссылке. Тем не менее, это делает мир различий. Действительно, теперь мы просим компилятор сделать копию n и передать эту копию f. Однако n не является constexpr, поэтому его значение известно только во время выполнения. Как компилятор мог сделать копию переменной, значение которой известно только во время выполнения? Конечно, не может. Действительно, сообщение об ошибке, данное Клэнгом, довольно ясно говорит о том, что происходит:

    error: constexpr variable 'i' must be initialized by a constant expression
    constexpr int i = f(n);
    ^ ~~~~
    note: read of non-const variable 'n' is not allowed in a constant expression
    constexpr int i = f(n);
    ^
    Todo:
    Explain how side-effects may not appear inside constant expressions, even if the expression they yield are not accessed.

    Appendix II: A minimal MPL


    В этом разделе представлена мини-реализация библиотеки MPL. Цель состоит в том, чтобы быть максимально обратно совместимым с MPL, все еще используя Hana под капотом. В качестве кейса реализована только часть «Алгоритмов» МПЛ, но реализовать многие (но не все) метафункции МПЛ должно быть возможно.

    Прокрутите вниз до функции main, чтобы увидеть тесты. Тесты являются примерами в документации MPL, которые были скопированы/пастированы, а затем изменены как можно меньше, чтобы работать с этим.

    // Copyright Louis Dionne 2013-2016
    // Distributed under the Boost Software License, Version 1.0.
    // (See accompanying file LICENSE.md or copy at http://boost.org/LICENSE_1_0.txt)
    #include <boost/hana.hpp>
    #include <boost/mpl/lambda.hpp>
    #include <boost/mpl/placeholders.hpp>
    #include <boost/mpl/quote.hpp>
    #include <iostream>
    #include <type_traits>
    namespace hana = boost::hana;
    namespace mpl = boost::mpl;
    namespace hpl {
    //////////////////////////////////////////////////////////////////////////////
    // Utilities
    //////////////////////////////////////////////////////////////////////////////
    namespace detail {
    template <typename Pred>
    constexpr auto mpl_predicate = hana::integral(hana::metafunction_class<
    typename mpl::lambda<Pred>::type
    >);
    template <typename F>
    constexpr auto mpl_metafunction = hana::metafunction_class<
    typename mpl::lambda<F>::type
    >;
    }
    //////////////////////////////////////////////////////////////////////////////
    // integral_c
    //////////////////////////////////////////////////////////////////////////////
    template <typename T, T v>
    using integral_c = std::integral_constant<T, v>;
    template <int i>
    using int_ = integral_c<int, i>;
    template <long i>
    using long_ = integral_c<long, i>;
    template <bool b>
    using bool_ = integral_c<bool, b>;
    using true_ = bool_<true>;
    using false_ = bool_<false>;
    //////////////////////////////////////////////////////////////////////////////
    // Sequences, compile-time integers & al
    //
    // Differences with the MPL:
    // 1. `pair<...>::first` and `pair<...>::second` won't work;
    // use `first<pair<...>>` instead
    //////////////////////////////////////////////////////////////////////////////
    template <typename ...T>
    using vector = hana::tuple<hana::type<T>...>;
    template <typename T, T ...v>
    using vector_c = hana::tuple<hana::integral_constant<T, v>...>;
    template <typename T, T from, T to>
    using range_c = decltype(hana::range_c<T, from, to>);
    template <typename T, typename U>
    using pair = hana::pair<hana::type<T>, hana::type<U>>;
    template <typename P>
    struct first : decltype(+hana::first(P{})) { };
    template <typename P>
    struct second : decltype(+hana::second(P{})) { };
    //////////////////////////////////////////////////////////////////////////////
    // Miscellaneous metafunctions
    //////////////////////////////////////////////////////////////////////////////
    template <typename C1, typename C2>
    struct equal_to
    : bool_<C1::value == C2::value>
    { };
    template <typename C1, typename C2>
    struct less
    : bool_<(C1::value < C2::value)>
    { };
    template <typename C1, typename C2>
    struct greater
    : bool_<(C1::value > C2::value)>
    { };
    template <typename N>
    struct next
    : integral_c<typename N::value_type, N::value + 1>
    { };
    //////////////////////////////////////////////////////////////////////////////
    // Intrinsics
    //
    // Differences with the MPL:
    // 1. `at` does not work for associative sequences; use `find` instead.
    // 2. `begin`, `end`, `clear`, `erase`, `erase_key`, `insert`, `insert_range`,
    // `is_sequence`, `key_type`, `order`, `sequence_tag`, `value_type`: not implemented
    //////////////////////////////////////////////////////////////////////////////
    template <typename Sequence, typename N>
    struct at
    : decltype(hana::at(Sequence{}, N{}))
    { };
    template <typename Sequence, long n>
    using at_c = at<Sequence, long_<n>>;
    template <typename Sequence>
    struct back
    : decltype(+hana::back(Sequence{}))
    { };
    template <typename Sequence>
    struct empty
    : decltype(hana::is_empty(Sequence{}))
    { };
    template <typename Sequence>
    struct front
    : decltype(+hana::front(Sequence{}))
    { };
    template <typename Sequence>
    struct pop_back {
    using type = decltype(hana::drop_back(
    hana::to_tuple(Sequence{}), hana::size_c<1>
    ));
    };
    template <typename Sequence>
    struct pop_front {
    using type = decltype(hana::drop_front(Sequence{}));
    };
    template <typename Sequence, typename T>
    struct push_back {
    using type = decltype(hana::append(Sequence{}, hana::type_c<T>));
    };
    template <typename Sequence, typename T>
    struct push_front {
    using type = decltype(hana::prepend(Sequence{}, hana::type_c<T>));
    };
    template <typename Sequence>
    struct size
    : decltype(hana::length(Sequence{}))
    { };
    //////////////////////////////////////////////////////////////////////////////
    // Iteration algorithms
    //
    // Differences with the MPL:
    // 1. reverse_fold:
    // Does not take an optional additional ForwardOp argument.
    //
    // 2. iter_fold, reverse_iter_fold:
    // Not implemented because we don't use iterators
    //////////////////////////////////////////////////////////////////////////////
    template <typename Sequence, typename State, typename F>
    struct fold
    : decltype(hana::fold(
    Sequence{}, hana::type_c<State>, detail::mpl_metafunction<F>
    ))
    { };
    template <typename Sequence, typename State, typename F>
    : decltype(hana::reverse_fold(
    Sequence{}, hana::type_c<State>, detail::mpl_metafunction<F>
    ))
    { };
    template <typename Sequence, typename State, typename F>
    using accumulate = fold<Sequence, State, F>;
    //////////////////////////////////////////////////////////////////////////////
    // Query algorithms
    //
    // Differences with the MPL:
    // 1. find_if and find:
    // Instead of returning an iterator, they either have a nested `::type`
    // alias to the answer, or they have no nested `::type` at all, which
    // makes them SFINAE-friendly.
    //
    // 2. lower_bound, upper_bound:
    // Not implemented.
    //
    // 3. {min,max}_element:
    // Not returning an iterator, and also won't work on empty sequences.
    //////////////////////////////////////////////////////////////////////////////
    template <typename Sequence, typename Pred>
    struct find_if
    : decltype(hana::find_if(Sequence{}, detail::mpl_predicate<Pred>))
    { };
    template <typename Sequence, typename T>
    struct find
    : decltype(hana::find(Sequence{}, hana::type_c<T>))
    { };
    template <typename Sequence, typename T>
    struct contains
    : decltype(hana::contains(Sequence{}, hana::type_c<T>))
    { };
    template <typename Sequence, typename T>
    struct count
    : decltype(hana::count(Sequence{}, hana::type_c<T>))
    { };
    template <typename Sequence, typename Pred>
    struct count_if
    : decltype(hana::count_if(Sequence{}, detail::mpl_predicate<Pred>))
    { };
    template <typename Sequence, typename Pred = mpl::quote2<less>>
    struct min_element
    : decltype(hana::minimum(Sequence{}, detail::mpl_predicate<Pred>))
    { };
    template <typename Sequence, typename Pred = mpl::quote2<less>>
    struct max_element
    : decltype(hana::maximum(Sequence{}, detail::mpl_predicate<Pred>))
    { };
    template <typename S1, typename S2, typename Pred = mpl::quote2<std::is_same>>
    struct equal
    : decltype( // inefficient but whatever
    hana::length(S1{}) == hana::length(S2{}) &&
    hana::all(hana::zip_shortest_with(detail::mpl_predicate<Pred>,
    hana::to_tuple(S1{}),
    hana::to_tuple(S2{})))
    )
    { };
    //////////////////////////////////////////////////////////////////////////////
    // Transformation algorithms
    //
    // Differences from the MPL:
    // 1. The algorithms do not accept an optional inserter, and they always
    // return a `vector`.
    // 2. stable_partition: not implemented
    // 3. All the reverse_* algorithms are not implemented.
    //////////////////////////////////////////////////////////////////////////////
    template <typename Sequence>
    struct copy {
    using type = decltype(hana::to_tuple(Sequence{}));
    };
    template <typename Sequence, typename Pred>
    struct copy_if {
    using type = decltype(hana::filter(
    hana::to_tuple(Sequence{}),
    detail::mpl_predicate<Pred>
    ));
    };
    template <typename Sequence, typename Sequence_or_Op, typename = void>
    struct transform;
    template <typename Sequence, typename Op>
    struct transform<Sequence, Op> {
    using type = decltype(hana::transform(
    hana::to_tuple(Sequence{}), detail::mpl_metafunction<Op>
    ));
    };
    template <typename S1, typename S2, typename Op>
    struct transform {
    using type = decltype(hana::zip_with(
    detail::mpl_metafunction<Op>,
    hana::to_tuple(S1{}),
    hana::to_tuple(S2{})
    ));
    };
    template <typename Sequence, typename OldType, typename NewType>
    struct replace {
    using type = decltype(hana::replace(
    hana::to_tuple(Sequence{}),
    hana::type_c<OldType>,
    hana::type_c<NewType>
    ));
    };
    template <typename Sequence, typename Pred, typename NewType>
    struct replace_if {
    using type = decltype(hana::replace_if(
    hana::to_tuple(Sequence{}),
    detail::mpl_predicate<Pred>,
    hana::type_c<NewType>
    ));
    };
    template <typename Sequence, typename T>
    struct remove {
    using type = decltype(hana::filter(
    hana::to_tuple(Sequence{}),
    hana::not_equal.to(hana::type_c<T>)
    ));
    };
    template <typename Sequence, typename Pred>
    struct remove_if {
    using type = decltype(hana::filter(
    hana::to_tuple(Sequence{}),
    hana::compose(hana::not_, detail::mpl_predicate<Pred>)
    ));
    };
    template <typename Sequence, typename Pred>
    struct unique {
    using type = decltype(hana::unique(
    hana::to_tuple(Sequence{}),
    detail::mpl_predicate<Pred>
    ));
    };
    template <typename Sequence, typename Pred>
    struct partition {
    using hana_pair = decltype(hana::partition(
    hana::to_tuple(Sequence{}),
    detail::mpl_predicate<Pred>
    ));
    using type = pair<
    decltype(hana::first(hana_pair{})),
    decltype(hana::second(hana_pair{}))
    >;
    };
    template <typename Sequence, typename Pred = mpl::quote2<less>>
    struct sort {
    using type = decltype(hana::sort(
    hana::to_tuple(Sequence{}), detail::mpl_predicate<Pred>
    ));
    };
    template <typename Sequence>
    struct reverse {
    using type = decltype(hana::reverse(hana::to_tuple(Sequence{})));
    };
    //////////////////////////////////////////////////////////////////////////////
    // Runtime algorithms
    //////////////////////////////////////////////////////////////////////////////
    template <typename Sequence, typename F>
    void for_each(F f) {
    hana::for_each(Sequence{}, [&f](auto t) {
    f(typename decltype(t)::type{});
    });
    }
    template <typename Sequence, typename TransformOp, typename F>
    void for_each(F f) {
    for_each<typename transform<Sequence, TransformOp>::type>(f);
    }
    } // end namespace hpl
    template <typename N>
    struct is_odd
    : hpl::bool_<(N::value % 2)>
    { };
    int main() {
    using namespace hpl;
    //////////////////////////////////////////////////////////////////////////////
    // Misc
    //////////////////////////////////////////////////////////////////////////////
    // pair
    {
    static_assert(std::is_same<first<pair<int, float>>::type, int>{}, "");
    static_assert(std::is_same<second<pair<int, float>>::type, float>{}, "");
    }
    //////////////////////////////////////////////////////////////////////////////
    // Intrinsics
    //////////////////////////////////////////////////////////////////////////////
    // at
    {
    using range = range_c<long,10,50>;
    static_assert(at<range, int_<0>>::value == 10, "");
    static_assert(at<range, int_<10>>::value == 20, "");
    static_assert(at<range, int_<40>>::value == 50, "");
    }
    // at_c
    {
    using range = range_c<long, 10, 50>;
    static_assert(at_c<range, 0>::value == 10, "");
    static_assert(at_c<range, 10>::value == 20, "");
    static_assert(at_c<range, 40>::value == 50, "");
    }
    // back
    {
    using range1 = range_c<int,0,1>;
    using range2 = range_c<int,0,10>;
    using range3 = range_c<int,-10,0>;
    using types = vector<int, char, float>;
    static_assert(back<range1>::value == 0, "");
    static_assert(back<range2>::value == 9, "");
    static_assert(back<range3>::value == -1, "");
    static_assert(std::is_same<back<types>::type, float>{}, "");
    }
    // empty
    {
    using empty_range = range_c<int,0,0>;
    using types = vector<long,float,double>;
    static_assert(empty<empty_range>{}, "");
    static_assert(!empty<types>{}, "");
    }
    // front
    {
    using types1 = vector<long>;
    using types2 = vector<int,long>;
    using types3 = vector<char,int,long>;
    static_assert(std::is_same<front<types1>::type, long>{}, "");
    static_assert(std::is_same<front<types2>::type, int>{}, "");
    static_assert(std::is_same<front<types3>::type, char>{}, "");
    }
    // pop_back
    {
    using types1 = vector<long>;
    using types2 = vector<long,int>;
    using types3 = vector<long,int,char>;
    using result1 = pop_back<types1>::type;
    using result2 = pop_back<types2>::type;
    using result3 = pop_back<types3>::type;
    static_assert(size<result1>::value == 0, "");
    static_assert(size<result2>::value == 1, "");
    static_assert(size<result3>::value == 2, "");
    static_assert(std::is_same< back<result2>::type, long>{}, "");
    static_assert(std::is_same< back<result3>::type, int>{}, "");
    }
    // pop_front
    {
    using types1 = vector<long>;
    using types2 = vector<int,long>;
    using types3 = vector<char,int,long>;
    using result1 = pop_front<types1>::type;
    using result2 = pop_front<types2>::type;
    using result3 = pop_front<types3>::type;
    static_assert(size<result1>::value == 0, "");
    static_assert(size<result2>::value == 1, "");
    static_assert(size<result3>::value == 2, "");
    static_assert(std::is_same<front<result2>::type, long>{}, "");
    static_assert(std::is_same<front<result3>::type, int>{}, "");
    }
    // push_back
    {
    using bools = vector_c<bool,false,false,false,true,true,true,false,false>;
    using message = push_back<bools, false_>::type;
    static_assert(back<message>::type::value == false, "");
    static_assert(count_if<message, equal_to<mpl::_1, false_>>{} == 6u, "");
    }
    // push_front
    {
    using v = vector_c<int,1,2,3,5,8,13,21>;
    static_assert(size<v>{} == 7u, "");
    using fibonacci = push_front<v, int_<1>>::type;
    static_assert(size<fibonacci>{} == 8u, "");
    static_assert(equal<
    fibonacci,
    vector_c<int,1,1,2,3,5,8,13,21>,
    equal_to<mpl::_, mpl::_>
    >{}, "");
    }
    // size
    {
    using empty_list = vector<>;
    using numbers = vector_c<int,0,1,2,3,4,5>;
    using more_numbers = range_c<int,0,100>;
    static_assert(size<empty_list>{} == 0u, "");
    static_assert(size<numbers>{} == 6u, "");
    static_assert(size<more_numbers>{} == 100u, "");
    }
    //////////////////////////////////////////////////////////////////////////////
    // Iteration algorithms
    //////////////////////////////////////////////////////////////////////////////
    // fold
    {
    using types = vector<long,float,short,double,float,long,long double>;
    using number_of_floats = fold<types, int_<0>,
    mpl::if_<std::is_floating_point<mpl::_2>,
    next<mpl::_1>,
    mpl::_1
    >
    >::type;
    static_assert(number_of_floats{} == 4, "");
    }
    // reverse_fold
    {
    using numbers = vector_c<int,5,-1,0,-7,-2,0,-5,4>;
    using negatives = vector_c<int,-1,-7,-2,-5>;
    using result = reverse_fold<numbers, vector_c<int>,
    mpl::if_<less<mpl::_2, int_<0>>,
    push_front<mpl::_1, mpl::_2>,
    mpl::_1
    >
    >::type;
    static_assert(equal<negatives, result>{}, "");
    }
    //////////////////////////////////////////////////////////////////////////////
    // Query algorithms
    //////////////////////////////////////////////////////////////////////////////
    // find_if
    {
    using types = vector<char,int,unsigned,long,unsigned long>;
    using found = find_if<types, std::is_same<mpl::_1, unsigned>>::type;
    static_assert(std::is_same<found, unsigned>{}, "");
    }
    // find
    {
    using types = vector<char,int,unsigned,long,unsigned long>;
    static_assert(std::is_same<find<types, unsigned>::type, unsigned>{}, "");
    }
    // contains
    {
    using types = vector<char,int,unsigned,long,unsigned long>;
    static_assert(!contains<types, bool>{}, "");
    }
    // count
    {
    using types = vector<int,char,long,short,char,short,double,long>;
    static_assert(count<types, short>{} == 2u, "");
    }
    // count_if
    {
    using types = vector<int,char,long,short,char,long,double,long>;
    static_assert(count_if<types, std::is_floating_point<mpl::_>>{} == 1u, "");
    static_assert(count_if<types, std::is_same<mpl::_, char>>{} == 2u, "");
    static_assert(count_if<types, std::is_same<mpl::_, void>>{} == 0u, "");
    }
    // min_element (MPL's example is completely broken)
    {
    }
    // max_element (MPL's example is completely broken)
    {
    }
    // equal
    {
    using s1 = vector<char,int,unsigned,long,unsigned long>;
    using s2 = vector<char,int,unsigned,long>;
    static_assert(!equal<s1,s2>{}, "");
    }
    //////////////////////////////////////////////////////////////////////////////
    // Transformaton algorithms
    //////////////////////////////////////////////////////////////////////////////
    // copy
    {
    using numbers = vector_c<int,10, 11, 12, 13, 14, 15, 16, 17, 18, 19>;
    using result = copy<range_c<int, 10, 20>>::type;
    static_assert(size<result>{} == 10u, "");
    static_assert(equal<result, numbers, mpl::quote2<equal_to>>{}, "");
    }
    // copy_if
    {
    using result = copy_if<range_c<int, 0, 10>, less<mpl::_1, int_<5>>>::type;
    static_assert(size<result>{} == 5u, "");
    static_assert(equal<result, range_c<int, 0, 5>>{}, "");
    }
    // transform
    {
    using types = vector<char,short,int,long,float,double>;
    using pointers = vector<char*,short*,int*,long*,float*,double*>;
    using result = transform<types,std::add_pointer<mpl::_1>>::type;
    static_assert(equal<result, pointers>{}, "");
    }
    // replace
    {
    using types = vector<int,float,char,float,float,double>;
    using expected = vector<int,double,char,double,double,double>;
    using result = replace< types,float,double >::type;
    static_assert(equal<result, expected>{}, "");
    }
    // replace_if
    {
    using numbers = vector_c<int,1,4,5,2,7,5,3,5>;
    using expected = vector_c<int,1,4,0,2,0,0,3,0>;
    using result = replace_if<numbers, greater<mpl::_, int_<4>>, int_<0>>::type;
    static_assert(equal<result, expected, mpl::quote2<equal_to>>{}, "");
    }
    // remove
    {
    using types = vector<int,float,char,float,float,double>;
    using result = hpl::remove<types, float>::type;
    static_assert(equal<result, vector<int, char, double>>{}, "");
    }
    // remove_if
    {
    using numbers = vector_c<int,1,4,5,2,7,5,3,5>;
    using result = remove_if<numbers, greater<mpl::_, int_<4> > >::type;
    static_assert(equal<result, vector_c<int,1,4,2,3>, mpl::quote2<equal_to>>{}, "");
    }
    // unique
    {
    using types = vector<int,float,float,char,int,int,int,double>;
    using expected = vector<int,float,char,int,double>;
    using result = unique<types, std::is_same<mpl::_1, mpl::_2>>::type;
    static_assert(equal<result, expected>{}, "");
    }
    // partition
    {
    using r = partition<range_c<int,0,10>, is_odd<mpl::_1>>::type;
    static_assert(equal<first<r>::type, vector_c<int,1,3,5,7,9>>{}, "");
    static_assert(equal<second<r>::type, vector_c<int,0,2,4,6,8>>{}, "");
    }
    // sort
    {
    using numbers = vector_c<int,3,4,0,-5,8,-1,7>;
    using expected = vector_c<int,-5,-1,0,3,4,7,8>;
    using result = sort<numbers>::type;
    static_assert(equal<result, expected, equal_to<mpl::_, mpl::_>>{}, "");
    }
    // reverse
    {
    using numbers = vector_c<int,9,8,7,6,5,4,3,2,1,0>;
    using result = reverse<numbers>::type;
    static_assert(equal<result, range_c<int,0,10>>{}, "");
    }
    //////////////////////////////////////////////////////////////////////////////
    // Runtime algorithms
    //////////////////////////////////////////////////////////////////////////////
    // for_each
    {
    auto value_printer = [](auto x) {
    std::cout << x << '\n';
    };
    for_each<range_c<int, 0, 10> >(value_printer);
    }
    }

    Статья Boost.Hana: User Manual раздела может быть полезна для разработчиков на c++ и boost.




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



    :: Главная :: ::


    реклама


    ©KANSoftWare (разработка программного обеспечения, создание программ, создание интерактивных сайтов), 2007
    Top.Mail.Ru

    Время компиляции файла: 2024-08-30 11:47:00
    2025-05-19 17:06:23/0.094923973083496/1