Change a function's return type based on size of std::tuple

Viewed 78

Problem

The following is a simplified, contrived example of an issue that I'm facing. Essentially, I need an object that can hold an arbitrary number of items, and return those items when needed.

template<typename... Ts>
class Foo {
public:
    Foo(Ts... args) :
        mArgs{std::forward<Ts>(args)} {
    }

    std::tuple<Ts...> getArgs() const {
        return mArgs;
    }

private:
    std::tuple<Ts...> mArgs;
};

This works fine when args > 1.

Foo<int, int> f{1, 2};
auto result = f.getArgs(); // result is a tuple containing two ints.

However, if args == 1, I would prefer to NOT get a tuple from getArgs.

Foo<int> f{1};
auto result = f.getArgs(); // result is a tuple, but I want it to be an int.

Question

Is there a way, perhaps using SFINAE, to define another getArgs function that is used when args == 1? Something like (and this is obviously very wrong):

template<typename = std::enable_if_t<std::tuple_size_v<Ts...> == 1>>
??? getArgs() const {
    return mArgs;
}

Several obvious problems with this:

  1. I'm not certain if std::tuple_size_v<Ts...> will even work.
  2. I don't know what the new return type will be. Perhaps std::tuple_element would be useful here.
2 Answers

A different approach: Use sizeof...(T) == 1 to determine whether only a single type is in use. Then use the auto return type and if constexpr to do the rest.


template<typename... T>
class Foo {
public:
    Foo(T&&... t)
        : mArgs{std::forward<T>(t)...} {
        
    }
    
    auto getArgs() const {
        if constexpr (sizeof...(T) == 1) {
            return std::get<0>(mArgs);
        } else {
            return mArgs;
        }
    }
  
private:
    std::tuple<T...> mArgs;
};

Example usage:

auto foo_i = Foo{11};
auto foo_c = Foo{'c'};
auto foo_ic = Foo{11, 'c'};

int i2 = foo_i.getArgs();
char c2 = foo_c.getArgs();
std::tuple<int, char> ic2 = foo_ic.getArgs();

Live Demo

You can actually do this with partial specializations for a given set of types. No SFINAE required. Here's how you would do it with the int example:

#include <stdio.h>
#include <tuple>

template<typename... Ts>
struct storage {
    typedef std::tuple<Ts...> type;
};

template<typename T>
struct storage<T> {
    typedef T type;
};

template<typename... Ts>
class Foo {
public:
    Foo(Ts... args) :
        mArgs{std::forward<Ts>(args)...} {
    }

    typename storage<Ts...>::type getArgs() const {
        return mArgs;
    }

private:
    typename storage<Ts...>::type mArgs;
};

int main(int argc, char **argv)
{
    Foo<int, int> f1{1, 2};
    Foo<int> f2{1};

    std::tuple<int, int> args1 = f1.getArgs();
    printf("args1 = (%d, %d)\n", std::get<0>(args1), std::get<1>(args1));
    int args2 = f2.getArgs();
    printf("args2 = %d\n", args2);

    return 0;
}
Related