Я пытаюсь использовать выражения сгиба С++ 17 и трюк с индексами С++ 14, чтобы сгладить произвольный ввод, состоящий из кортежей и некортежей.
Ожидаемый результат должен как минимум соответствовать этим требованиям:
constexpr auto bare = 42;
constexpr auto single = std::tuple{bare};
constexpr auto nested_simple = std::tuple{single};
constexpr auto multiple = std::tuple{bare, bare};
constexpr auto nested_multiple = std::tuple{multiple};
constexpr auto multiply_nested = std::tuple{multiple, multiple};
static_assert(flatten(bare) == bare);
static_assert(flatten(single) == bare);
static_assert(flatten(nested_simple) == bare);
static_assert(flatten(multiple) == multiple);
static_assert(flatten(nested_multiple) == multiple);
static_assert(flatten(multiply_nested) == std::tuple{bare, bare, bare, bare});
У меня есть относительно простой код для обработки всех случаев, кроме последнего:
template<typename T>
constexpr decltype(auto) flatten(T&& t)
{
return std::forward<T>(t);
}
template<typename T>
constexpr decltype(auto) flatten(std::tuple<T> t)
{
return std::get<0>(t);
}
template<typename... Ts>
constexpr decltype(auto) flatten_multi(Ts&&... ts)
{
return std::make_tuple(flatten(ts)...);
}
template<typename... Ts, std::size_t... Indices>
constexpr decltype(auto) flatten_impl(std::tuple<Ts...> ts, const std::index_sequence<Indices...>&)
{
return flatten_multi(std::get<Indices>(ts)...);
}
template<typename... Ts>
constexpr decltype(auto) flatten(std::tuple<Ts...> ts)
{
return flatten_impl(ts, std::make_index_sequence<sizeof...(Ts)>());
}
Живая демонстрация здесь. Очевидно, что он плохо обрабатывает несколько вложенных элементов.
Более продвинутой формы для обработки случая multiply_nested я не нашел. Я пытался применить operator>>, чтобы иметь возможность использовать выражения сгиба, но не смог получить ничего, что компилировалось. Моя последняя попытка может быть найдена здесь. Основная идея состоит в том, чтобы использовать operator>> в выражении свертки для объединения элементов 2 на 2, каждый раз разворачивая предыдущий результат.
Мне кажется, что я должен быть в состоянии использовать что-то вроде std::tuple_cat, но он кричал на меня довольно громко по причинам, которые я не мог полностью расшифровать.
Итак, мой вопрос заключается в следующем: что мне не хватает? Как я могу развернуть произвольно глубоко произвольно вложенный ввод, похожий на кортеж?
@ Барри Хорошая мысль! Все это время мне было интересно, был ли make_tuple заменен выводом аргумента шаблона класса. Думаю, нет.
Просто для уточнения: правильно ли, что std::tuple<int&> должно быть сведено к std::tuple<int&>, а std::tuple<std::tuple<int, double>&> должно быть сведено к std::tuple<std::tuple<int, double>&> (т. е. ссылки нетронуты)?
@Julius Мне кажется, на данный момент да, ссылки должны оставаться нетронутыми. Если в качестве входных данных была дана ссылка, ссылка должна выйти.





namespace flattenns {
struct flat_t {};
template<std::size_t... Is, class...As>
constexpr auto flatten( std::index_sequence<Is...>, flat_t, std::tuple<As...> as ) {
return std::tuple_cat( flatten(flat_t{}, std::get<Is>(as))... );
}
template<class...As, class...Ts>
constexpr auto flatten( flat_t, std::tuple<As...> as ) {
return flatten( std::make_index_sequence<sizeof...(As)>{}, flat_t{}, as );
}
template<class T>
constexpr std::tuple<T> flatten( flat_t, T t ) { return {t}; }
template<class...Ts>
constexpr auto flatten( flat_t, Ts... ts ) {
return std::tuple_cat( flatten(flat_t{}, ts)... );
}
constexpr std::tuple<> flatten( flat_t ) { return {}; }
}
template<class...Ts>
constexpr auto sane_flatten( Ts...ts ) {
return flattenns::flatten(flattenns::flat_t{}, ts...);
}
// to take std::tuple<int>(7) -> 7
namespace insanens {
template<class...Ts>
constexpr auto unpack_single( std::tuple<Ts...> t ) {return t;}
template<class T>
constexpr auto unpack_single( std::tuple<T> t ) {return std::get<0>(t);}
}
template<class...Ts>
constexpr auto insane_flatten( Ts...ts ) {
return insanens::unpack_single( sane_flatten(ts...) );
}
template<class...Ts>
constexpr auto flatten( Ts...ts ) {
return insane_flatten(ts...);
}
Как отмечалось выше, flatten( std::tuple<int>(7) ) должен НЕ БЫТЬ 7. Это безумие.
Но, как вы хотите, я добавляю это как шаг постобработки.
В остальном ваша операция относительно нормальная. Вы рекурсивно применяете [[x],[y]] к [x,y]. Окончательная распаковка не вменяемая. Разделив его, код становится легким, что также доказывает, почему он безумен.
Если вам интересно, тип тега flat_t существует для того, чтобы (а) отделить последовательность индексов от возможного аргумента (что можно сделать, используя другое имя функции) и (б) включить поиск ADL, чтобы каждая реализация flatten могла увидеть все остальные.
Тот факт, что сведение одного кортежа в его элемент имеет смысл для моего варианта использования (я не пытаюсь сгладить кортежи напрямую, а набор классов, которые могут рекурсивно содержать экземпляры друг друга, один из которых должен вести себя следующим образом ).
@rubenvb Мне нечего тебе сказать, кроме как «иди изучай Haskell и монады». То, что вы делаете, похоже на «Я хочу сложить два целых числа и получить результат. Однако если целые числа равны 7 и 21, вместо этого создайте 0x314159». Конечно, для этого есть варианты использования, но это должно быть сделано как отдельный шаг, а не частью вашей функции «сложения целых чисел», и, вы должны дважды проверить, действительно ли ваш вариант использования разумен. Кроме того, изменено, чтобы уменьшить рекурсивную глубину до рекурсивной глубины кортежей, а не общей длины вывода.
Математическое свойство, которое я моделирую, — это ассоциативность: (a) = a имеет математический смысл. Обычный случай (a+b)+c = a+b+c несколько полезнее, да. Помимо этого безумия: могут ли выражения свертки каким-либо образом улучшить производительность во время компиляции (т. е. уменьшить количество экземпляров рекурсивных шаблонов) или я лаю не по тому дереву, желая использовать это здесь?
@ruben За исключением того, что вы просите = что-то, что даже не является выражением. Потому что (a+b)+cуже в кортеже в этой модели -- "внешние скобки" выражения не записываются. EXPR(EXPR(a,b),c) = EXPR(a,b,c) в здравом уме, EXPR(a)=EXPR(a) в здравом уме, EXPR(a)=a далеко в левом поле. Я понимаю, почему вы хотите это сделать, и почему это чувствует себя в здравом уме, я же говорю вам, что путь драконов лежит. В любом случае моя рекурсивная глубина равна O (рекурсивная глубина ввода), поэтому уменьшение рекурсивной глубины возможно только до постоянного коэффициента. Здесь помогут выражения двойной складки.
вы вполне можете быть правы (и, вероятно, так). Спасибо за ваше время, я точно не понял, чего мне не хватало, но это придет со временем однажды/пока я преобразую это в свой собственный код и типы.
как flatten(42) == 42, для меня нормально, что flatten(std::tuple<int>(42)) == 42 (или flatten(std::tuple<std::tuple<int>>(42)) == 42).
@Jarod42 flatten(42) == std::tuple{42} на самом деле. Я полагаюсь на это, чтобы упростить первую перегрузку flatten. Я думаю, что мой flatten в основном рекурсивный монадический бинд.
Предлагаю SFINAE при наличии tuple
// Simple traits
template <typename T> struct is_tuple : std::false_type{};
template <typename... Ts> struct is_tuple<std::tuple<Ts...>> : std::true_type{};
// utility to ensure return type is a tuple
template<typename T>
constexpr decltype(auto) as_tuple(T t) { return std::make_tuple(t); }
template<typename ...Ts>
constexpr decltype(auto) as_tuple(std::tuple<Ts...> t) { return t; }
// Simple case
template<typename T>
constexpr decltype(auto) flatten(T t)
{
return t;
}
// Possibly recursive tuple
template<typename T>
constexpr decltype(auto) flatten(std::tuple<T> t)
{
return flatten(std::get<0>(t));
}
// No more recursion, (sizeof...Ts != 1) with above overload
template<typename ...Ts, std::enable_if_t<!(is_tuple<Ts>::value || ...), bool> = false>
constexpr decltype(auto) flatten(std::tuple<Ts...> t)
{
return t;
}
// Handle recursion
template<typename ...Ts, std::enable_if_t<(is_tuple<Ts>::value || ...), bool> = false>
constexpr decltype(auto) flatten(std::tuple<Ts...> t)
{
return std::apply([](auto...ts)
{
return flatten(std::tuple_cat(as_tuple(flatten(ts))...));
}, t);
}
Есть ли способ сделать этот идеальный форвард... все возможное? Я хотел бы предотвратить как можно больше копий (и перемещений) и предотвратить материализацию временных файлов в такой операции сглаживания.
@rubenvb: Это должно быть возможно. хитрость в том, чтобы SFINAE их для std::tuple так T&& не поймать std::tuple&
Что-то, возможно, немного более простое, хотя и более подробное: специализация шаблона частичного класса + if constexpr:
Основной подход заключается в специализации следующего базового класса:
template<class... T>
struct flatten
{};
Чтобы учесть наши три случая:
tuple одной вещиtuple из более чем одной вещиСлучай № 1, базовый случай, довольно прост, просто верните то, что мы получили:
//base case: something that isn't another tuple
template<class T>
struct flatten<T>
{
template<class U>
constexpr decltype(auto) operator()(U&& _value){
return std::forward<U>(_value);
}
};
Случай № 2 также довольно прост, просто повторяйте саму себя, пока не дойдете до случая № 1.
// recursive case 1 : plain old tuple of one item
template<class T>
struct flatten<std::tuple<T>>
{
template<class U>
constexpr decltype(auto) operator()(U&& _tup){
return flatten<std::remove_cvref_t<T>>{}(std::get<0>(_tup));
}
};
Случай № 3 длинный из-за возможных подкейсов, но каждый блок довольно читабелен. Мы
И тогда у нас есть четыре случая для рассмотрения:
tuple<int, int>, tuple<int, int>)tuple<int, int>, int)int, tuple<int, int>)int, int)Нам просто нужна одна вспомогательная функция, которая позволит нам удалить заголовок кортежа и вернуть его остальную часть.
// helper for getting tuple elements except the first one
template<template<class...> class Tup, class... T, size_t... indices>
constexpr auto get_rest_of_tuple(const Tup<T...>& _tup, std::index_sequence<indices...>){
return std::make_tuple(std::get<indices + 1>(_tup)...);
}
и некоторые вспомогательные черты:
// some type traits to use for if constexpr
template<class T>
struct is_tuple : std::false_type{};
template<class... T>
struct is_tuple<std::tuple<T...>> : std::true_type{};
template<class T>
constexpr bool is_tuple_v = is_tuple<T>::value;
Наконец импл:
// recursive case 2: tuple of more than one item
template<class First, class Second, class... Rest>
struct flatten<std::tuple<First, Second, Rest...>>
{
template<class Tup>
constexpr decltype(auto) operator()(Tup&& _tup){
auto flattened_first = flatten<std::remove_cvref_t<First>>{}(std::get<0>(_tup));
auto restTuple = get_rest_of_tuple(_tup, std::make_index_sequence<sizeof...(Rest)+1>{});
auto flattened_rest = flatten<std::remove_cvref_t<decltype(restTuple)>>{}(restTuple);
// both are tuples
if constexpr(is_tuple_v<decltype(flattened_first)> && is_tuple_v<decltype(flattened_rest)>)
{
return std::tuple_cat(flattened_first, flattened_rest);
}
// only second is tuple
if constexpr(!is_tuple_v<decltype(flattened_first)> && is_tuple_v<decltype(flattened_rest)>)
{
return std::tuple_cat(std::make_tuple(flattened_first), flattened_rest);
}
//only first is tuple
if constexpr(is_tuple_v<decltype(flattened_first)> && !is_tuple_v<decltype(flattened_rest)>)
{
return std::tuple_cat(flattened_first, std::make_tuple(flattened_rest));
}
// neither are tuples
if constexpr(!is_tuple_v<decltype(flattened_first)> && !is_tuple_v<decltype(flattened_rest)>)
{
return std::tuple_cat(std::make_tuple(flattened_first), std::make_tuple(flattened_rest));
}
}
};
} // namespace detail
Наконец, мы используем трамплининг, чтобы скрыть все эти детали от конечного пользователя, поместив их в пространство имен details и выставив следующую функцию для их вызова:
template<class T>
constexpr decltype(auto) flatten(T&& _value){
return detail::flatten<std::remove_cvref_t<T>>{}(std::forward<T>(_value));
}
(включает некоторые дополнительные тесты на правильность)
Хотя реализация случая № 3 выше довольно проста, она одновременно многословна и немного неэффективна (компилятор оценивает каждый из этих if constexpr операторов, когда он должен оценивать только один, но я не хотел цеплять по else ветвям из-за вложенности ).
Мы можем значительно упростить случай № 3, обратившись к двум вспомогательным функциям, которые определяют, является ли аргумент кортежем not, и возвращают правильное значение:
template<class U, std::enable_if_t<!is_tuple_v<U>, int> = 0>
constexpr decltype(auto) flatten_help(U&& _val){
return std::make_tuple(_val);
}
template<class... T>
constexpr decltype(auto) flatten_help(const std::tuple<T...>& _tup){
return _tup;
}
// recursive case 2: tuple of more than one item
template<class First, class Second, class... Rest>
struct flatten<std::tuple<First, Second, Rest...>>
{
template<class Tup>
constexpr decltype(auto) operator()(Tup&& _tup){
auto flattened_first = flatten<std::remove_cvref_t<First>>{}(std::get<0>(_tup));
auto restTuple = get_rest_of_tuple(_tup, std::make_index_sequence<sizeof...(Rest)+1>{});
auto flattened_rest = flatten<std::remove_cvref_t<decltype(restTuple)>>{}(restTuple);
return std::tuple_cat(flatten_help(flattened_first), flatten_help(flattened_rest));
}
};
Вот еще одна версия с две цели дизайна:
std::tuple_catЧтобы избежать временных кортежей и std::tuple_cat, полезно предсказать окончательный размер выходного кортежа. Давайте определим помощника с именем get_rank:
#include <cstddef>
#include <tuple>
#include <type_traits>
template<class T>
struct Type {// tag type
using type = T;
};
template<class T>
constexpr std::size_t get_rank(Type<T>) {
static_assert(!std::is_const<T>{} && !std::is_volatile<T>{}, "avoid surprises");
return 1;
}
template<class... Ts>
constexpr std::size_t get_rank(Type< std::tuple<Ts...> >) {
return (0 + ... + get_rank(Type<Ts>{}));
}
Функция flatten может использовать get_rank для создания последовательности индексов для элементов выходного кортежа. Эта последовательность передается в flatten_impl вместе с переданным входным кортежем и тегом типа. Давайте явно предоставим перегрузки lvalue и rvalue для функции интерфейса, но внутри используем совершенную пересылку:
#include <cstddef>
#include <tuple>
#include <utility>
// to be implemented
#include "tuple_element_at_rankpos_t.hpp"
#include "get_at_rankpos.hpp"
template<std::size_t... rank_positions, class Tuple, class... Ts>
constexpr auto flatten_impl(
std::index_sequence<rank_positions...>,
Tuple&& tuple,
Type< std::tuple<Ts...> > tuple_tag
) {
return std::tuple<
tuple_element_at_rankpos_t< rank_positions, std::tuple<Ts...> >...
>{
get_at_rankpos<rank_positions>(std::forward<Tuple>(tuple), tuple_tag)...
};
}
template<class... Ts>
constexpr auto flatten(const std::tuple<Ts...>& tuple) {
using TupleTag = Type< std::tuple<Ts...> >;
constexpr std::size_t rank = get_rank(TupleTag{});
return flatten_impl(
std::make_index_sequence<rank>{}, tuple, TupleTag{}
);
}
template<class... Ts>
constexpr auto flatten(std::tuple<Ts...>& tuple) {
using TupleTag = Type< std::tuple<Ts...> >;
constexpr std::size_t rank = get_rank(TupleTag{});
return flatten_impl(
std::make_index_sequence<rank>{}, tuple, TupleTag{}
);
}
template<class... Ts>
constexpr auto flatten(std::tuple<Ts...>&& tuple) {
using TupleTag = Type< std::tuple<Ts...> >;
constexpr std::size_t rank = get_rank(TupleTag{});
return flatten_impl(
std::make_index_sequence<rank>{}, std::move(tuple), TupleTag{}
);
}
На данный момент нам нужны еще два строительных блока:
tuple_element_at_rankpos_t (как std::tuple_element_t, но для вложенных кортежей) иget_at_rankpos (как std::get, но для вложенных кортежей).Любой строительный блок должен найти тип/значение элемента во вложенном входном кортеже на основе позиции элемента в сглаженном выходном кортеже. На каждом уровне вложенности эти строительные блоки должны извлекать индекс текущей глубины вложенности из rankpos. Это общее вычисление индекса можно переместить в помощник extract_index. Первый строительный блок может выглядеть так:
#include <cassert>
#include <cstddef>
#include <array>
#include <tuple>
#include <utility>
template<class... Ts>
constexpr auto extract_index(
std::size_t rankpos, Type< std::tuple<Ts...> >
) {
static_assert(sizeof...(Ts) >= 1, "do not extract from empty tuples");
constexpr auto ranks = std::array{get_rank(Type<Ts>{})...};
std::size_t index = 0;
std::size_t nested_rankpos = rankpos;
while(nested_rankpos >= ranks[index]) {
nested_rankpos -= ranks[index++];
assert(index < sizeof...(Ts));
}
return std::pair{index, nested_rankpos};
}
////////////////////////////////////////////////////////////////////////////////
template<std::size_t rankpos, class T>
constexpr auto tuple_element_at_rankpos_tag(
Type<T> /* element_tag */
) {
static_assert(rankpos == 0);
return Type<T>{};
}
template<std::size_t rankpos, class... Ts>
constexpr auto tuple_element_at_rankpos_tag(
Type< std::tuple<Ts...> > tuple_tag
) {
// constexpr auto [index, nested_rankpos] = extract_index(rankpos, tuple_tag);
constexpr std::pair pair = extract_index(rankpos, tuple_tag);
constexpr std::size_t index = pair.first;
constexpr std::size_t nested_rankpos = pair.second;
using NestedType = std::tuple_element_t< index, std::tuple<Ts...> >;
return tuple_element_at_rankpos_tag<nested_rankpos>(
Type<NestedType>{}
);
}
template<std::size_t rankpos, class Tuple>
using tuple_element_at_rankpos_t = typename decltype(
tuple_element_at_rankpos_tag<rankpos>(Type<Tuple>{})
)::type;
Второй строительный блок — это повторение того же связующего кода, что и выше. Помимо типа нам нужно обрабатывать значения (lvalue, const lvalue, rvalue). Используя совершенную переадресацию, мы можем написать:
template<std::size_t rankpos, class Element, class T>
constexpr decltype(auto) get_at_rankpos(
Element&& element,
Type<T> /* element_tag */
) {
static_assert(rankpos == 0);
return std::forward<Element>(element);
}
template<std::size_t rankpos, class Tuple, class... Ts>
constexpr decltype(auto) get_at_rankpos(
Tuple&& tuple,
Type< std::tuple<Ts...> > tuple_tag
) {
// constexpr auto [index, nested_rankpos] = extract_index(rankpos, tuple_tag);
constexpr std::pair pair = extract_index(rankpos, tuple_tag);
constexpr std::size_t index = pair.first;
constexpr std::size_t nested_rankpos = pair.second;
using NestedType = std::tuple_element_t< index, std::tuple<Ts...> >;
return get_at_rankpos<nested_rankpos>(
std::get<index>(std::forward<Tuple>(tuple)),
Type<NestedType>{}
);
}
Интересный подход. Мне интересно, сколько временных кортежей осталось в С++ 17. Вероятно, многое из-за того, что значения кортежа перетасовываются в новые кортежи. Мне нравится полномасштабная идея «давайте полностью сгладим эту вещь с самого начала». Что-то вроде помещения возвращаемого функцией значения в место назначения на месте вызова.
Обратите внимание, что этот
constexpr auto nested_simple = std::tuple{single};делает не то, что вы думаете.