Как сгладить разнородные списки (также известные как кортежи кортежей...)

Я пытаюсь использовать выражения сгиба С++ 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, но он кричал на меня довольно громко по причинам, которые я не мог полностью расшифровать.

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

Обратите внимание, что этот constexpr auto nested_simple = std::tuple{single}; делает не то, что вы думаете.

Barry 28.02.2019 19:16

@ Барри Хорошая мысль! Все это время мне было интересно, был ли make_tuple заменен выводом аргумента шаблона класса. Думаю, нет.

rubenvb 28.02.2019 19:19

Просто для уточнения: правильно ли, что std::tuple<int&> должно быть сведено к std::tuple<int&>, а std::tuple<std::tuple<int, double>&> должно быть сведено к std::tuple<std::tuple<int, double>&> (т. е. ссылки нетронуты)?

Julius 01.03.2019 13:03

@Julius Мне кажется, на данный момент да, ссылки должны оставаться нетронутыми. Если в качестве входных данных была дана ссылка, ссылка должна выйти.

rubenvb 01.03.2019 19:30
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
8
4
626
4
Перейти к ответу Данный вопрос помечен как решенный

Ответы 4

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 28.02.2019 19:01

@rubenvb Мне нечего тебе сказать, кроме как «иди изучай Haskell и монады». То, что вы делаете, похоже на «Я хочу сложить два целых числа и получить результат. Однако если целые числа равны 7 и 21, вместо этого создайте 0x314159». Конечно, для этого есть варианты использования, но это должно быть сделано как отдельный шаг, а не частью вашей функции «сложения целых чисел», и, вы должны дважды проверить, действительно ли ваш вариант использования разумен. Кроме того, изменено, чтобы уменьшить рекурсивную глубину до рекурсивной глубины кортежей, а не общей длины вывода.

Yakk - Adam Nevraumont 28.02.2019 19:05

Математическое свойство, которое я моделирую, — это ассоциативность: (a) = a имеет математический смысл. Обычный случай (a+b)+c = a+b+c несколько полезнее, да. Помимо этого безумия: могут ли выражения свертки каким-либо образом улучшить производительность во время компиляции (т. е. уменьшить количество экземпляров рекурсивных шаблонов) или я лаю не по тому дереву, желая использовать это здесь?

rubenvb 28.02.2019 19:10

@ruben За исключением того, что вы просите = что-то, что даже не является выражением. Потому что (a+b)+cуже в кортеже в этой модели -- "внешние скобки" выражения не записываются. EXPR(EXPR(a,b),c) = EXPR(a,b,c) в здравом уме, EXPR(a)=EXPR(a) в здравом уме, EXPR(a)=a далеко в левом поле. Я понимаю, почему вы хотите это сделать, и почему это чувствует себя в здравом уме, я же говорю вам, что путь драконов лежит. В любом случае моя рекурсивная глубина равна O (рекурсивная глубина ввода), поэтому уменьшение рекурсивной глубины возможно только до постоянного коэффициента. Здесь помогут выражения двойной складки.

Yakk - Adam Nevraumont 28.02.2019 19:16

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

rubenvb 28.02.2019 19:22

как flatten(42) == 42, для меня нормально, что flatten(std::tuple<int>(42)) == 42 (или flatten(std::tuple<std::tuple<int>>(42)) == 42).

Jarod42 28.02.2019 20:19

@Jarod42 flatten(42) == std::tuple{42} на самом деле. Я полагаюсь на это, чтобы упростить первую перегрузку flatten. Я думаю, что мой flatten в основном рекурсивный монадический бинд.

Yakk - Adam Nevraumont 28.02.2019 21:13
Ответ принят как подходящий

Предлагаю 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 28.02.2019 20:47

@rubenvb: Это должно быть возможно. хитрость в том, чтобы SFINAE их для std::tuple так T&& не поймать std::tuple&

Jarod42 01.03.2019 09:59

Что-то, возможно, немного более простое, хотя и более подробное: специализация шаблона частичного класса + if constexpr:

Основной подход заключается в специализации следующего базового класса:

template<class... T>
struct flatten
{};

Чтобы учесть наши три случая:

  1. Голая стоимость
  2. tuple одной вещи
  3. 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 длинный из-за возможных подкейсов, но каждый блок довольно читабелен. Мы

  • Сгладить первый элемент (возможно, рекурсивно)
  • Сгладить остальные элементы (возможны рекурсии)

И тогда у нас есть четыре случая для рассмотрения:

  1. У нас есть два кортежа (например, tuple<int, int>, tuple<int, int>)
  2. У нас есть кортеж и значение (например, tuple<int, int>, int)
  3. У нас есть значение и кортеж (например, int, tuple<int, int>)
  4. У нас есть два значения (например, 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));
    }
};

Демо 2

Вот еще одна версия с две цели дизайна:

  1. избегайте построения временных кортежей и избегайте std::tuple_cat
  2. явно определить типы в конечном кортеже

Чтобы избежать временных кортежей и 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. Вероятно, многое из-за того, что значения кортежа перетасовываются в новые кортежи. Мне нравится полномасштабная идея «давайте полностью сгладим эту вещь с самого начала». Что-то вроде помещения возвращаемого функцией значения в место назначения на месте вызова.

rubenvb 04.03.2019 09:25

Другие вопросы по теме