5
\$\begingroup\$

In my project, I want to search for an item of a given type in a std::tuple at compile time. The given type might not be present in the tuple at all, in which case some invalid value should be returned to signal that. I'm using c++17 by the way.

template<typename Arg, typename... Args>
struct contains {
    static constexpr bool value = std::disjunction_v<std::is_same<Arg, Args>...>;
};
template<typename... Args>
struct Stuffs {
    std::tuple<Args...> data;

    template<typename Arg>
    std::enable_if_t<contains<Arg, Args...>::value, Arg*>
    constexpr search() {
        auto& ref = std::get<Arg>(data);
        return &ref;
    }
    template<typename Arg>
    std::enable_if_t<!contains<Arg, Args...>::value, Arg*>
    constexpr search() {
        return nullptr;
    }
};

int main() {
    struct A { int val = 100; };
    struct B {};
    struct C {};
    struct D {};
    struct E {};

    if constexpr (Stuffs<A, B, C, D>{}.search<A>()->val == 100) {
        std::cout << "A found" << std::endl;
    }
    if constexpr (Stuffs<A, B, C, D>{}.search<E>()) {
        std::cout << "should not happen" << std::endl;
    }
    if constexpr (Stuffs<A, B, C, D>{}.search<int>()) {
        std::cout << "should not happen" << std::endl;
    }
}

This code seems to work fine in terms of searching in a tuple at compile time. Initially, I tried to use std::variant and void* but their operations are mostly prohibited in a constexpr setting. Then I arrived at this approach. I'm new to C++ template programming, so I'm wondering if there are any potential issues with this code and how I may improve it?

\$\endgroup\$
5
  • \$\begingroup\$ I mean… technically you are allowed to put the declaration specifiers in any order… but I’ve never seen anyone put the constexpr after the return type unless they were being deliberately silly. 🤷🏼 \$\endgroup\$
    – indi
    Dec 10, 2023 at 16:25
  • \$\begingroup\$ @indi I felt it was more readable than constexpr enable_if_t <...> \$\endgroup\$
    – dw218192
    Dec 10, 2023 at 18:39
  • \$\begingroup\$ I’m very wary of claims that code is more “readable” when it violates universal conventions. Put another way: more readable… for whom? Someone who doesn’t really know C++? Or someone who knows it very well? Consider: I have been C++ing since before the first standard, and read/write both beginner and advanced C++ code roughly every day… and I was flummoxed for a minute, had to stop and remember the grammar rules and reason it through, before I could accept it. Programming is communication; speaking in strange ways, even if it makes more sense to you, is communicating poorly. \$\endgroup\$
    – indi
    Dec 12, 2023 at 21:56
  • \$\begingroup\$ I’d also add that if you used more modern C++ features, your readability problem goes away. For example, with trailing return syntax (which is superior in almost every way) and C++20 requires (which makes enable_if obsolete), you could do: template <typename Arg>⏎requires contains<Arg, Args...>::value⏎constexpr auto search() -> Arg*. There are no rules, of course, so if you prefer your way, stick with it. As long as you’re consistent within a particular codebase, it’s fine. \$\endgroup\$
    – indi
    Dec 12, 2023 at 21:56
  • \$\begingroup\$ @indi well said, I appreciate your insights and great response. I'll keep this in mind when writing code in the future. \$\endgroup\$
    – dw218192
    Dec 13, 2023 at 2:00

2 Answers 2

6
\$\begingroup\$

This is a great idea, and is pretty well-done. Not bad for someone new to metaprogramming!

I have only two major suggestions for improving contains.

The first is to either have it inherit from std::bool_constant, or to-reimplement all the stuff in bool_constant.

That’s because bool_constant is more than just its value. There are other handy conversions baked in. This is (roughly) what bool constant looks like:

template <bool V>
struct bool_constant
{
    using type       = bool_constant;
    using value_type = bool;

    static constexpr bool value = V;

    constexpr auto operator()() const noexcept { return V; }

    constexpr operator auto() const noexcept { return V; }
};

You’ve got the value, but none of the other features. They are not often used, sure, but since you get them for free, you might as well do:

template <typename Arg, typename... Args>
struct contains 
    : std::bool_constant<std::disjunction_v<std::is_same<Arg, Args>...>>
{};

You could also do:

template <typename Arg, typename... Args>
struct contains 
    : std::disjunction<std::is_same<Arg, Args>...>
{};

But I don’t recommend that, because a std::disjunction can boil down to a random type that may not support the full interface. And the whole point is you want the full interface.

The second major suggestion is to change the interface slightly to make it easier to use.

What I would like to do is write:

using my_tuple = std::tuple<int, float, std::string>

if (contains<my_tuple, int>::value)
    // ...

// Or:
if (contains<std::tuple<int, float, std::string>, int>::value)
    // ...

But I can’t write that. In fact, I can’t use contains at all without additional boilerplate to hoist the tuple’s types out of the tuple. contains itself is like three lines, but you need more than double that (in the Stuffs class) to be able to actually use it meaningfully.

What you should do is use partial specialization to extract the types automatically, like so:

// Primary template. Leave undefined, so it only works with tuples or
// tuple-like stuff.
template <typename Haystack, typename Needle>
struct contains;

// Specialization for std::tuple:
template <typename Needle, typename... Args>
struct contains<std::tuple<Args...>, Needle>
    : std::bool_constant<std::disjunction_v<std::is_same<Needle, Args>...>>
{};

// Or,  better: specialization for any kind of type list or anything tuple-like:
template <template <typename...> typename Haystack, typename Needle, typename... Args>
struct contains<Haystack<Args...>, Needle>
    : std::bool_constant<std::disjunction_v<std::is_same<Needle, Args>...>>
{};

Now you no longer need Stuffs to use the contains metafunction… though you could still do it if you want:

template <typename... Args>
struct Stuffs
{
    std::tuple<Args...> data;

    // Don't really need enable_if for this.
    template <typename Arg>
    constexpr auto search() -> Arg*
    {
        if constexpr (contains<std::tuple<Args...>, Arg>::value)
            return &std::get<Arg>(data);
        else
            return nullptr;
    }
};

Now, all that being said, I suggest that you may have solved the wrong problem.

If all you want is to test whether some type is in a tuple, then fine, nailed it.

Howeveris that all you want?

Because even in your sample code, you want more. You not only want to know whether a type is in a tuple… you want to access that type.

And since you want that, you end up doing the work twice. First you search the tuple to find the type. Then you search it again (via std::get<Arg>()) to get the type. That’s a waste of effort. You already found it… why forget about it then search for it again?

You may argue that the efficiency waste doesn’t matter, because all this is happening at compile time, and that is true. However, there is another problem. Try this code:

auto main() -> int
{
    if (Stuffs<int, int>{}.search<int>() != nullptr)
        std::cout << "found!\n";
    else
        std::cout << "not found\n";
}

The problem isn’t your code, it’s std::get<T>(). That fails if you use a type that exists more than once in the tuple. You know there’s in an int in there… but you can’t access it.

So I would suggest that the problem that you really want to solve is not whether Arg is in tuple<Args...>, but where Arg is in tuple<Args...>.

So instead of just a contains function which returns a bool, what you actually want is a find function that returns an index.

Suppose you did have this find metafunction:

template <typename Haystack, typename Needle>
struct find;

// Returns the index of the first occurence of Needle, or size_t{-1}.
template <template <typename...> typename Haystack, typename Needle, typename... Args>
struct find<Haystack<Args...>, Needle>
    : std::integral_constant<std::size_t, /* magic! */>
{};

Now your Stuffs operation becomes:

template <typename... Args>
struct Stuffs
{
    std::tuple<Args...> data;

    // Don't really need enable_if for this.
    template <typename Arg>
    constexpr auto search() -> Arg*
    {
        constexpr auto index = find<tuple<Args...>, Arg>::value;

        if constexpr (index != std::size_t{-1})
            return &std::get<index>(data);
        else
            return nullptr;
    }
};

And this will always work, even for tuples with duplicate types. And you’re only searching the tuple once.

In fact, the find operation is fundamental, so you can even write contains in terms of it:

template <typename Haystack, typename Needle>
struct contains
    : std::bool_constant<find<Haystack, Needle>::value != std::size_t{-1}>
{};

In fact, you could go even further, and have a metafunction find_all that returns a std::index_sequence with the indices of every occurrence of a needle. So:

find_all<tuple<int, float, char, int, int, double, int>, int>
// value_type is index_sequence<0, 3, 4, 6>

find_all<tuple<int, float, char, int, int, double, int>, double>
// value_type is index_sequence<5>

find_all<tuple<int, float, char, int, int, double, int>, short>
// value_type is index_sequence<>

And then make find in terms of that (whenever the length of the index sequence is non-zero, take the first index). And contains would still be in terms of find. Plus you could also write another metafunction count that counts the number of times a type is in a tuple… that could be in terms of find_all too. Plus you could easily write a find_last. And a unique. And so on.

Have fun!

\$\endgroup\$
5
\$\begingroup\$

An interesting question, and already an excellent answer by indi. I just want to add:

Make it a get_if<>() for tuples

For std::variant they made a std::get_if<>() function that is basically what you want, except it doesn't work for tuples. However, you could modify your code to do that:

template<typename Arg, typename... Args>
struct contains {
    static constexpr bool value = std::disjunction_v<std::is_same<Arg, Args>...>;
};

template<typename Arg, typename... Args>
std::enable_if_t<contains<Arg, Args...>::value, Arg*>
constexpr get_if(std::tuple<Args...>&& tuple) {
    return &std::get<Arg>(tuple);    
}

template<typename Arg, typename... Args>
Arg*
constexpr get_if(std::tuple<Args...>&& tuple) {
    return nullptr;
}

And use it like so:

struct A { int val = 100; };
struct B {};
struct C {};
struct D {};
struct E {};

using Stuffs = std::tuple<A, B, C, D>;

if constexpr (get_if<A>(Stuffs{})->val == 100)
    std::cout << "A found\n";
if constexpr (get_if<E>(Stuffs{}))
    std::cout << "should not happen\n";
if constexpr (get_if<int>(Stuffs{}))
    std::cout << "should not happen\n";

Just like indi's contains and find(), it's now a free function that works on regular std::tuples, no need to write a wrapper class.

It still has the issue that it doesn't handle tuples with duplicate types, but then again, std::get_if() for std::variants doesn't handle that case either, so there is at least no surprising behavior here.

Note that tuples might be passed as const and non-const lvalue references and as rvalues, the above code doesn't handle all cases. This is left as an excercise for the reader.

Use '\n' instead of std::endl

Prefer using '\n' instead of std::endl; the latter is equivalent to the former, but also forces the output to be flushed, which is usually not nececssary and negatively impacts performance.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.