1

I've got some hierarchy of std::tuple. I would like to write some access functions for all these tuples so that the resulting code is more readable. So instead of writing:

std::get<2>(std::get<1>(std::get<0>(s)))

I would rather prefer to write

getNetName(getFirstNet(getFirstOutput(s)))

Now the point is to avoid writing these access functions twice -- for constant parameters and for writeable parameters. Can this be done? And of course -- I want these access functions to be type-safe. There might be multiple tuples types to which std::get<0>() can be applied -- but I would rather prefer that getNetName() creates a compiler error if it is not applied to a net.

  • If you are using getters and setters your getter should not allow you to modify the state so you could always return by value or `const&`. – NathanOliver Jul 10 '18 at 14:20
  • 4
    This seems to be a pretty convoluted approach to workaround the use of tuples instead of actual classes. Why not avoid trouble and make the code typesafe, easy to read and more idiomatic at the small cost of once writing some trivial classes? – Voo Jul 10 '18 at 14:20
  • A tupple is a struct with unnamed elements. So what is the sense of using a tuple and writing extra functions to get named access? That seems to be the exact opposite of the sense of a tuple. – Klaus Jul 10 '18 at 15:52
  • Guys, if this problem (and the solution) is not appreciated, I'm going to delete it. –  Jul 11 '18 at 17:20

2 Answers2

3

std::get already does exactly what you want, so your goal is to simply write an alias for an existing function. The trick is to perfect forward your argument and return value to ensure nothing is lost. For example :

#include <tuple>

template<class T>
decltype(auto) getFirstElem(T&& p_tuple)
{
    return std::get<0>(std::forward<T>(p_tuple));
}

int main()
{
    std::tuple<int, int> mutable_x = { 42, 24 };
    const std::tuple<int, int> const_x = { 10, 20 };

    // mutable_ref is a `int&`
    auto&& mutable_ref = getFirstElem(mutable_x);

    // const_ref is a `const int&`
    auto&& const_ref = getFirstElem(const_x);
}

decltype(auto) ensure that the return value is perfect forwarded. This preserves the reference qualifier and the constness of the return type. Using auto would cause the return value to decay to the underlying value type (in this case int). auto&& is similarly used to capture the result without discarding reference qualifier or constness.

Edit : It seems there was a type safety component to the question I missed. This is easily fixed by introducing a static_assert and std::is_same to compare the argument type with the expected type. It's important to remove reference qualifiers and cv modifiers to ensure the comparison is correct.

template<class T>
decltype(auto) getFirstElem(T&& p_tuple)
{
    using t_expected = std::tuple<int, int>;

    // Verify that the tuple matches the expectations
    using t_tuple_clean = std::remove_cv_t<std::remove_reference_t<T>>;
    static_assert(std::is_same<t_expected, t_tuple_clean>::value, "Unexpected tuple type");

    return std::get<0>(std::forward<T>(p_tuple));
}

Unfortunately, the error message will usually be pretty long. Unfortunately I don't see a way to write this where the compiler's built-in argument matching could be used (which would generate clearer error messages). Perfect forwarding requires that the argument be a template type. Otherwise, you would need two overloads (one for const and one for non-const arguments) which would violate the single-function requirement of the question.

If you find it annoying to write out the check for every function, you can write a helper which can be used to more easily write new access functions.

#include <tuple>
#include <type_traits>

template<size_t index, class t_expected, class t_tuple>
decltype(auto) getHelper(t_tuple&& p_tuple)
{
    // Verify that the tuple matches the expectations
    using t_tuple_clean = std::remove_cv_t<std::remove_reference_t<t_tuple>>;
    static_assert(std::is_same<t_expected, t_tuple_clean>::value, "Unexpected tuple type");

    // Forward to std::get
    return std::get<index>(std::forward<t_tuple>(p_tuple));
}


template<class T>
decltype(auto) getFirstElem(T&& p_tuple)
{
    return getHelper<0, std::tuple<int, int>>(std::forward<T>(p_tuple));
}

The access function will now fail with a compiler error if the wrong type of tuple is provided :

int main()
{
    // Compiler error 'Unexpected tuple type'
    std::tuple<double, int> bad_tuple{};
    auto&& bad_ref = getFirstElem(bad_tuple);
}
François Andrieux
  • 28,148
  • 6
  • 56
  • 87
  • not type safe. getFirstElem() can be applied to any tuple type with more than 0 elements. Missing the point. –  Jul 10 '18 at 14:35
  • Maybe you should elaborate what you mean by type-safe in the question. For example, what is a "net"? Is it a shape of tuple? The type of one of the tuple elements? Something else? – Useless Jul 10 '18 at 14:37
  • @FrankPuck Fair enough, I missed that part of the question. I'll update my answer. – François Andrieux Jul 10 '18 at 14:41
  • @FrankPuck Are you trying to limit what type of tuples get passed as arguments or are you trying to limit the input types to those that have the expected element types at the given index? – François Andrieux Jul 10 '18 at 14:44
  • getNetName() should only compile if the argument is a net –  Jul 10 '18 at 14:47
  • @FrankPuck To be clear, if a "net" is a type of `tuple`, there is no way to discriminate it from other `tuple`s with the same member types. Type safety can only get you so far in this case. – François Andrieux Jul 10 '18 at 14:48
  • @FrankPuck in that case the solution is to make `net` a class type, with meaningful members, not an alias for a particular `std::tuple` instantiation – Caleth Jul 10 '18 at 15:18
1

sorry this is C++11 (complain to my boss)!

template<typename T, std::size_t I>
struct get
{       auto operator()(T &_r) const -> decltype(std::get<I>(_r))
        {       return std::get<I>(_r);
        }
        auto operator()(const T &_r) const -> decltype(std::get<I>(_r))
        {       return std::get<I>(_r);
        }
};

and application:

typedef std::pair<
    std::size_t, // starting value
    std::size_t // size or ending value?
> dimension;
typedef get<dimension, 0> getDimensionStart;
typedef get<dimension, 1> getDimensionSize;
typedef std::pair<
    std::string,
    boost::optional<dimension>      // if scalar, then this is empty
> port;
typedef get<port, 0> getPortName;
typedef get<port, 1> getPortDimension;

using this code would look like this:

const port s("name", dimension(0, 10));
std::cout << getDimensionStart()(*getPortDimension()(s)) << std::endl;