Search code examples
c++11visual-studio-2013tuplesdecltypetrailing-return-type

Compile time access to tuple item with an enum value


I would like to use enum values as index to access to tuple items. I've read this post to help.
I'm working in VS2013 thus I cannot use C++14 very handy auto without trailing return.
Static cast works but it's cumbersome, also it requires tuple is freely accessed (in real code attributes are in protected scope).
I want to improve this by using a templated method GetAttribute.
I tried this but it produces an error:

prog.cpp:30:119: error: invalid operands of types '' and 'size_t {aka unsigned int}' to binary 'operator<' auto GetAttribute(AttributeName attributeName) -> decltype(std::declval(std::get(attributeName)>(attributes))) ^ prog.cpp:30:119: error: invalid operands of types '' and 'size_t {aka unsigned int}' to binary 'operator<' prog.cpp: In function 'int main()': prog.cpp:57:4: error: 'struct X' has no member named 'GetAttribute' x.GetAttribute(XParameters::PARAMETER1); // Does not compile. ^

Live demo

#include <tuple>
#include <cstddef>
#include <utility>
#include <iostream>

enum class XParameters : unsigned int
{
    PARAMETER1, // int
    PARAMETER2, // float
    MAX,
};

enum class YParameters : unsigned int
{
    PARAMETER3 = XParameters::MAX // std::string
};

using XTuple = std::tuple<int, float>;
using YAttributes = std::tuple<std::string>;
using YTuple = decltype(tuple_cat(XTuple(), YAttributes()));

template <typename Attributes>
struct Common
{
    Common(Attributes&& attr) : attributes(std::move(attr)) {}

    Attributes attributes;

    template <typename AttributeName>
    auto GetAttribute(AttributeName attributeName) -> decltype(std::declval(std::get<static_cast<size_t>(attributeName)>(attributes)))
    {
        return std::get<static_cast<size_t>(attributeName)>(attributes);
    }
};

struct X : Common<XTuple>
{
    X() : Common(std::make_tuple(42, 3.14f)) {}
};

struct Y : Common<YTuple>
{
    Y() : Common(std::make_tuple(666, 0.01f, "string")) {}
};

int main()
{
    X x;
    Y y;

    int parameter1 = std::get<static_cast<size_t>(XParameters::PARAMETER1)>(x.attributes); // Compiles, works.
    std::cout << parameter1 << std::endl;

    std::string parameter3 = std::get<static_cast<size_t>(YParameters::PARAMETER3)>(y.attributes); // Compiles, works.
    std::cout << parameter3 << std::endl;

    // Shorter code
    x.GetAttribute(XParameters::PARAMETER1); // Does not compile.

    //parameter3 = std::get<static_cast<size_t>(YParameters::PARAMETER3)>(x.attributes); // Does not compile, phew...

    return 0;
}

Solution

  • I am afraid there will be no pretty solution.

    Ugly variant - Cast at call site

    The easiest way to achieve something similar would be to pass the value as a non-type template parameter:

    template <size_t attributeName>
    auto GetAttribute() -> decltype(std::get<attributeName>(attributes))
    {
        return std::get<attributeName>(attributes);
    }
    

    This would make the call ugly because you would have to do the cast there:

    x.GetAttribute<static_cast<size_t>(XParameters::PARAMETER1)>();
    

    Not so ugly variant - associated enum type

    You could work around that by associating an enum type with the Common class like this:

    template <typename Attributes, typename EnumType>
    struct Common
    {
        Common(Attributes&& attr) : attributes(std::move(attr)) {}
    
        Attributes attributes;
    
        template <size_t attributeName>
        auto GetAttribute() -> decltype(std::get<attributeName>(attributes))
        {
            return std::get<attributeName>(attributes);
        }
    
        template <EnumType attributeName>
        auto GetAttribute() -> decltype(GetAttribute<static_cast<size_t>(attributeName)>())
        {
            return GetAttribute<static_cast<size_t>(attributeName)>();
        }
    
    };
    

    Which would allow for calling like that:

    struct X : Common<XTuple, XParameters>
    {
        X() : Common(std::make_tuple(42, 3.14f)) {}
    };
    
    x.GetAttribute<XParameters::PARAMETER1>();
    

    Obviously, you would need to dedicated an enum for each Common instance.

    Best variant(?) - Free (overloaded) function

    In this case you just use raw tuples. Then you roll your own function (e.g. GetAttribute) that you overload for your specific type of tuple and enum:

    template <XParameters attributeName>
    auto GetAttribute(XTuple &tuple) -> decltype(std::get<static_cast<size_t>(attributeName)>(tuple) )
    {
        return  std::get<static_cast<size_t>(attributeName)>(tuple);
    }
    
    GetAttribute<XParameters::PARAMETER1>(x.attributes);
    

    Defining GetAttribute in this case has a lot of boilerplate code so you might want to hide it behind a macro:

    #define DEFINE_GetAttribute(ENUM_TYPE, TUPLE_TYPE)                             \
      template <ENUM_TYPE attributeName>                                           \
      auto GetAttribute(TUPLE_TYPE &tuple)                                         \
          ->decltype(std::get<static_cast<size_t>(attributeName)>(tuple)) {        \
        return std::get<static_cast<size_t>(attributeName)>(tuple);                \
      }
    
    DEFINE_GetAttribute(XParameters, XTuple)