Home > Mobile >  Generalizing std::conditional_t<>
Generalizing std::conditional_t<>

Time:09-16

I have a function that computes a certain object from a given parameter (say, an important node from a graph). Now, when calculating such an object, the function might allocate some memory. Sometimes I want the function to just return the result, and sometimes to return the result plus the memory used to compute it.

I typically solve this binary case like this:

enum class what {
    what1,  // return, e.g., just an int
    what2   // return, e.g., a std::pair<int, std::vector<int>>
};

template <what w>
std::conditional_t<w == what::what1, int, std::pair<int, std::vector<int>>>
calculate_something(const param& p) { ... }

I would like to generalize the solution above to longer enumerations

enum class list_whats {
    what1,
    what2,
    what3,
    what4,
    what5
};

One possible solution is to nest as many std::conditional_t as needed

template <list_whats what>
std::conditional_t<
    what == list_whats::what1,
    int,
    std::conditional_t<
        what == list_whats::what2,
        float,
        ....
    >
>
>
calculate_something(const param& p) { ... }

But this is cumbersome and perhaps not too elegant.

Does anyone know how to do this in C 17?

CodePudding user response:

I don't think you should use std::conditional at all to solve your problem. If I get this right, you want to use a template parameter to tell your function what to return. The elegant way to do this could look something like this:

#include <vector>

enum class what { what1, what2 };

template <what W>
auto compute() {
  if constexpr (W == what::what1) {
    return 100;
  }
  if constexpr (W == what::what2) {
    return std::pair{100, std::vector<int>{}};
  }
}

auto main() -> int {
  [[maybe_unused]] const auto as_int = compute<what::what1>();
  [[maybe_unused]] const auto as_pair = compute<what::what2>();
}

You can also use template specialization if you prefer another syntax:

template <what W>
auto compute();

template <>
auto compute<what::what1>() {
  return 100;
}

template <>
auto compute<what::what2>() {
  return std::pair{100, std::vector<int>{}};
}

CodePudding user response:

I found a possible solution that nests two structs: the first takes a list of Boolean values to indicate which type should be used, and the nested struct takes the list of possible types (see conditional_list in the example code below -- the nested structs were inspired by this answer). But perhaps it's not elegant enough. I'm wondering if there is a possible solution of the form

std::conditional_list<
    ..., // list of Boolean values, (of any length!)
    ... // list of types           (list that should be as long as the first)
>::type

My proposal

#include <type_traits>
#include <iostream>
#include <vector>

// -----------------------------------------------------------------------------

template<auto A, auto... ARGS>
constexpr auto sum = A   sum<ARGS...>;
template<auto A>
constexpr auto sum<A> = A;

// -----------------------------------------------------------------------------

template <bool... values>
static constexpr bool exactly_one_v = sum<values...> == 1;

// -----------------------------------------------------------------------------

template <bool... values>
struct which {
    static_assert(exactly_one_v<values...>);

    template <std::size_t idx, bool v1, bool... vs>
    struct _which_impl {
        static constexpr std::size_t value =
            (v1 ? idx : _which_impl<idx   1, vs...>::value);
    };

    template <std::size_t idx, bool v>
    struct _which_impl<idx, v> {
        static constexpr std::size_t value = (v ? idx : idx   1);
    };

    static constexpr std::size_t value = _which_impl<0, values...>::value;
};

template <bool... conds>
static constexpr std::size_t which_v = which<conds...>::value;

// -----------------------------------------------------------------------------

template <std::size_t ith_idx, typename... Ts>
struct ith_type {
    template <std::size_t cur_idx, typename t1, typename... ts>
    struct _ith_type_impl {
        typedef
            std::conditional_t<
                ith_idx == cur_idx,
                t1,
                typename _ith_type_impl<cur_idx   1, ts...>::type
            >
            type;
    };

    template <std::size_t cur_idx, typename t1>
    struct _ith_type_impl<cur_idx, t1> {
        typedef
            std::conditional_t<ith_idx == cur_idx, t1, std::nullptr_t>
            type;
    };

    static_assert(ith_idx < sizeof...(Ts));
    typedef typename _ith_type_impl<0, Ts...>::type type;
};

template <std::size_t ith_idx, typename... ts>
using ith_type_t = typename ith_type<ith_idx, ts...>::type;

// -----------------------------------------------------------------------------

template <bool... conds>
struct conditional_list {
    template <typename... ts>
    struct good_type {
        static_assert(sizeof...(conds) == sizeof...(ts));
        typedef ith_type_t<which_v<conds...>, ts...> type;
    };
};

// -----------------------------------------------------------------------------

enum class list_whats {
    what1,
    what2,
    what3,
    what4,
    what5,
};

template <list_whats what>
typename conditional_list<
    what == list_whats::what1,
    what == list_whats::what2,
    what == list_whats::what3,
    what == list_whats::what4,
    what == list_whats::what5
>::template good_type<
    int,
    float,
    double,
    std::string,
    std::vector<int>
>::type
return_something() noexcept {
    if constexpr (what == list_whats::what1) { return 1; }
    if constexpr (what == list_whats::what2) { return 2.0f; }
    if constexpr (what == list_whats::what3) { return 3.0; }
    if constexpr (what == list_whats::what4) { return "42"; }
    if constexpr (what == list_whats::what5) { return {1,2,3,4,5}; }
}

int main() {
    auto s1 = return_something<list_whats::what1>();
    s1 = 3;

    auto s2 = return_something<list_whats::what2>();
    s2 = 4.0f;

    auto s3 = return_something<list_whats::what3>();
    s3 = 9.0;

    auto s4 = return_something<list_whats::what4>();
    s4 = "qwer";

    auto s5 = return_something<list_whats::what5>();
    s5[3] = 25;
}

CodePudding user response:

An alternative to working only with types is to write a function that returns the specific type, or an identity<type>. It's sometimes more readable. Here is an example:

    // if you don't have it in std
    template<typename T>
    struct identity {
        using type = T;
    };
     
    enum class what {
        what1,
        what2,
        what3
    };
     
    template<what w>
    auto return_type_for_calc() {
        if constexpr (w == what::what1) {
            return identity<int>();
        } else if constexpr (w==what::what2) {
            return identity<double>();
        } else {
            return identity<float>();
        }
    }
     
    template<what w>
    decltype(return_type_for_calc<w>())
    calculate_something()
    {
        return {};
    }
     
    int main() {
        calculate_something<what::what1>();
        calculate_something<what::what2>();
        return 0;
    }

CodePudding user response:

Here's my approach:

  • what_pair is a pair that corresponds one enum to one type.

  • what_type_index accepts a enum and a std::tuple<what_pair<...>...> and searches the tuple map where the enums are equal and returns index. It returns maximum std::size_t value, if no match was found.

  • what_type is the final type, it is the tuple element at the found position. The program won't compile when the index is std::size_t max value because of invalid std::tuple access.

template<what W, typename T>
struct what_pair {
    constexpr static what w = W;
    using type = T;
};

template<what w, typename tuple_map>
constexpr auto what_type_index() {
    std::size_t index = std::numeric_limits<std::size_t>::max();
    auto search_map = [&]<std::size_t... Ints>(std::index_sequence<Ints...>) {
        ((std::tuple_element_t<Ints, tuple_map>::w == w ? (index = Ints) : 0), ...);
    };
    search_map(std::make_index_sequence<std::tuple_size_v<tuple_map>>());
    return index;
}

template<what w, typename tuple_map>
using what_type = typename 
    std::tuple_element_t<what_type_index<w, tuple_map>(), tuple_map>::type;

and this is the example usage:

int main() {
    using what_map = std::tuple<
        what_pair<what::what1, int>,
        what_pair<what::what2, float>,
        what_pair<what::what3, double>,
        what_pair<what::what4, std::string>,
        what_pair<what::what5, std::vector<int>>>;

    static_assert(std::is_same_v<what_type<what::what1, what_map>, int>);
    static_assert(std::is_same_v<what_type<what::what2, what_map>, float>);
    static_assert(std::is_same_v<what_type<what::what3, what_map>, double>);
    static_assert(std::is_same_v<what_type<what::what4, what_map>, std::string>);
    static_assert(std::is_same_v<what_type<what::what5, what_map>, std::vector<int>>);

    //compilation error, because 'what6' wasn't specified in the 'what_map'
    using error = what_type<what::what6, what_map>;
}

try it out on godbolt.

  • Related