Search code examples
c++tuplesapplyc++20c++-concepts

It is possible to write concept requiring iteration over static tuple and checking each tuple item


I'm writing simple code for automatic reflection of struct. in short each struct have static tuple with name and member pointer and code iterate over it using std::apply and do some stuff.


struct TestRecord{
    int id;
    long counter; 
};

template<>
struct RepositoryHelperID<TestRecord>
{
    static constexpr auto members = [] {
        return std::make_tuple(
            std::make_tuple("id"sv, &TestRecord::id),
            std::make_tuple("counter"sv, &TestRecord::counter));
    }();
};

it is working but i have some helper function (getDBType) for each type of member needed to whole code to work. to minimize compilation error size in case of missing this helper function i decided to create concept which should:

  • iterate over static tuple
  • get type of member from tuple element
  • check if specific function exists for this type.

but i'm unable to make it work. if all function exist code compiles and it work. when function is missing i expect that concept is not fulfilled but instead i have compilation error about missing function. i assumed if function is missing then require is not meet and concept fails, but as far i deduced template function needs to be evaluated to then check require statement. i try multiple combination of lambda function in std::apply but none worked.

i tried :

  • lambda with require and empty body
  • making simpler jump function with expected require on it.

below extracted full example of idea. and compiler explorer link https://godbolt.org/z/zss8aTM13

the question is if it is possible to make concept using tuples and std::apply.

#include <array>
#include <iostream>
#include <tuple>

#include <string_view>
using namespace std::string_view_literals;

template <std::same_as<int> T>
std::string getDBType(){
    return "int";
}

template <std::same_as<double> T>
std::string getDBType(){
    return "double";
}

template<typename T>
struct RepositoryHelperID
{

};

struct TestRecord{
    int id;
    long counter; // change to double to compile 
};


template<>
struct RepositoryHelperID<TestRecord>
{
    static constexpr auto members = [] {
        return std::make_tuple(
            std::make_tuple("id"sv, &TestRecord::id),
            std::make_tuple("counter"sv, &TestRecord::counter));
    }();
};
template <typename T>
concept HaveGetType =
    requires {
        {
            getDBType<T>()
        };
    };



template <typename T>
concept RepositoryHelperConcept =
    requires(T) {
        {
            RepositoryHelperID<T>::members
        };
        {
            // std::remove_reference_t<decltype(std::declval<T>().*std::get<1>(args))> == int , double etc. so check if eg. getDBType<int>() exists
              std::apply([](auto&&... args) {
                 ((getDBType<std::remove_reference_t<decltype(std::declval<T>().*std::get<1>(args))>>()), ...);
            },
                RepositoryHelperID<T>::members)
            
        };
    };

int main(){
    ///static_assert(!RepositoryHelperConcept<TestRecord>);
    return !RepositoryHelperConcept<TestRecord>;  
}

Solution

  • std::apply is an unconstrained function, so the requires-clause will always trigger hard errors when the lambda passed in is ill-formed.

    However, you can still use std::apply to check whether the return type of each member pointer after being invoked by the object satisfies HaveGetType

    template <typename T>
    concept HaveGetType = requires {
      getDBType<T>();
    };
    
    template <typename T>
    concept RepositoryHelperConcept = requires {
      RepositoryHelperID<T>::members;
      requires std::apply([](auto&&... args) {
        return (
          HaveGetType<
            std::remove_reference_t<decltype(std::declval<T>().*std::get<1>(args))>
          > && ...);
      }, RepositoryHelperID<T>::members);
    };
    

    Demo