c++templatesc++17constexprfunction-templates

Can't create multiple constexpr getter


I have a struct which wraps multiple std::sets of data. I want access to the sets to be computed at compile time. To do this, I created a template constexpr getter method to return the requested set.

// simplified sample
struct TwoSets 
{
    template <int i> constexpr std::set<int> &get() noexcept 
    {
        if constexpr (i == 0) {
            return first;
        } else if constexpr (i == 1) {
            return second;
        } else {
            static_assert(i == 0 || i == 1, "invalid");
        }
    }
    
    std::set<int> first;
    std::set<int> second;
};

This works, but there are parts of the code that insert to a given set, and parts of the code that want read-only access to a set via a const reference to the set like so:

TwoSets sets;
sets.get<0>().insert(0);
    
// ...elsewhere in code
const TwoSets &const_sets = sets;
std::cout << const_sets.get<0>().size();

This results in an error:

error: passing ‘const TwoSets’ as ‘this’ argument discards qualifiers [-fpermissive]

This can be fixed by marking get as const/returning a const reference, which breaks the insertion code. What do I need to do to both be able to both

  1. Perform the set selection at compile time
  2. Access the sets with a mutable reference and with a const immutable reference

Solution

  • What do I need to do to both be able to both

    1. Perform the set selection at compile time.
    2. Access the sets with a mutable reference and with a const immutable reference.

    In deducing this is the perfect solution for such scenarios.

    struct TwoSets 
    {
       template <int i, typename Self>
       constexpr auto&& get(this Self&& self ) noexcept
       {
          if constexpr (i == 0) {
             return std::forward<Self>(self).first;
          }
          else if constexpr (i == 1) {
             return std::forward<Self>(self).second;
          }
          else {
             static_assert(i == 0 || i == 1, "invalid");
          }
       }
    
       std::set<int> first{};
       std::set<int> second{};
    };
    

    See example code


    However, prior to [tag:C++:23], you need to

    1. either provide both const and non-const getter members,
    2. or move the common logic to a non member friend or an internal function.

    moving the common logic to a function would look like:

    // Forward declaration
    struct TwoSets;
    // some internal namespace
    template<int i, typename T> constexpr auto& getSet(T&&) noexcept;
    
    struct TwoSets 
    {
       template <int i>
       constexpr std::set<int>& get() noexcept {
          return getSet<i>(*this);
       }
    
       template <int i>
       constexpr const std::set<int>& get() const noexcept {
          return getSet<i>(*this);
       }
    
       std::set<int> first;
       std::set<int> second;
    };
    
    // some internal namespace
    // Friend function to get the set from TwoSets
    template<int i, typename T>
    constexpr auto& getSet(T&& sets) noexcept
    {
       // ... code
    }
    

    See example code


    Side note: If your actual scenario is like shown, you should consider std::tuple as @paddy suggested in the comment, and Keep It Simple, & Stupid!"