0

I want to get all complete types that satisfy a certain concept. The types should be listed as element types of a std::tuple.

For:

#include <concepts>
#include <tuple>

class A{};

template<typename T>
concept InterestingType = std::derived_from<T, A>;

class B : public A{};
class C : public A{};
class D{};

I would want to get the type std::tuple<A, B, C> (because A, B and C satisfy the concept InterestingType but D doesn't.). The order of the classes in the tuple doesn't matter (e.g. std::tuple<C, B, A> is also fine). Duplicates would probably also be fine (e.g. std::tuple<A, B, C, A, A>).

Is this possible?

update: As asked in the comments, here the planned purpose of an answer to this question:

I have a number of predefined classes which have a string identifier and provide a parsing constructor which interprets a string, e.g.:

struct MyInt {
static constexpr char name[] = "MyInt";
int i_;
MyInt(std::string);
}
struct MyDouble {
static constexpr char name[] = "MyDouble";
double d_;
MyInt(std::string);
}

And a factory method:

std::any make(std::string type_name, std::string constructor_arg){
// generated code
if (type_name == MyInt::name)
  return MyInt{constructor_arg};
else if (type_name == MyDouble::name)
  return MyDouble{constructor_arg};
else 
  return {};
}

make calls actually a compile-time generated switch-case-like template function which takes a tuple of types (i.e. std::tuple<MyInt, MyDouble>) as template argument and generates code equivalent to the implementation provided above. I currently define the tuple of registered types manually. This is OK for predefined types shipped with the library.

What I am struggling with (and why I asked the first question), is how a user can provide additional types and register them.

final note: A virtual constructor is for other technical reasons no option. The registered types (like MyInt and MyDouble) cannot have virtual methods for other technical reasons.

update 2:

I've build an working example with the code below from HolyBlackCat's answer below. It compiles fine with clang >= 10 but fails on all gcc versions. Checkout https://godbolt.org/z/9xxbo79rr

Any idea what I need to change so that it builds with gcc? At least gcc 11 should be able to compile it.

#include <iostream>

#include <any>
#include <string>
#include <map>

using func_ptr = std::any (*)(std::string);
using func_map = std::map<std::string, func_ptr>;

struct Factory {
    inline static func_map &make_funcs() {
        static func_map func_ptrs;
        return func_ptrs;
    }
};

template<typename T>
struct Base {
    inline static const std::nullptr_t dummy = [] {
        // This code will run for each derived class, when the program starts.

        Factory::make_funcs()[T::name] = [](std::string s) -> std::any { return T{std::move(s)}; };
        return nullptr;
    }();

    // Force `dummy` to be instantiated, even though it's unused.
    static constexpr std::integral_constant<decltype(&dummy), &dummy> dummy_helper{};
};

struct MyInt : Base<MyInt> {
    static constexpr char name[] = "MyInt";
    int i_ = 5;

    MyInt(std::string) {}
};

struct MyDouble : Base<MyDouble> {
    static constexpr char name[] = "MyDouble";
    double d_ = 4.0;

    MyDouble(std::string) {}
};


int main() {
    for (auto &[_, make] : Factory::make_funcs()) {
        auto instance = make("a");
        try {
            any_cast<MyDouble>(instance);
            std::cout << "is MyDouble" << std::endl;
        } catch (...) {}

        try {
            any_cast<MyInt>(instance);
            std::cout << "is MyInt" << std::endl;
        } catch (...) {}
    }
}

update 3:

It works by making the init function of dummy a separate function which is not defined in the class. See https://godbolt.org/z/hnW58zc4s

#include <iostream>

#include <any>
#include <string>
#include <map>

using func_ptr = std::any (*)(std::string);
using func_map = std::map<std::string, func_ptr>;

struct Factory {
    inline static func_map &make_funcs() {
        static func_map func_ptrs;
        return func_ptrs;
    }
};

template<typename T>
struct Base {
    inline static std::nullptr_t init();
    inline static const std::nullptr_t dummy = init();

    // Force `dummy` to be instantiated, even though it's unused.
    static constexpr std::integral_constant<decltype(&dummy), &dummy> dummy_helper{};
};

template<typename T>
std::nullptr_t Base<T>::init() {
    Factory::make_funcs()[T::name] = [](std::string s) -> std::any { return T{std::move(s)}; };
    return nullptr;
}

struct MyInt : Base<MyInt> {
    static constexpr char name[] = "MyInt";
    int i_ = 5;

    MyInt(std::string) {}
};

struct MyDouble : Base<MyDouble> {
    static constexpr char name[] = "MyDouble";
    double d_ = 4.0;

    MyDouble(std::string) {}
};


int main() {
    for (auto &[_, make] : Factory::make_funcs()) {
        auto instance = make("a");
        try {
            any_cast<MyDouble>(instance);
            std::cout << "is MyDouble" << std::endl;
        } catch (...) {}

        try {
            any_cast<MyInt>(instance);
            std::cout << "is MyInt" << std::endl;
        } catch (...) {}
    }
}
A. Bigerl
  • 99
  • 8
  • 6
    No, not possible, at least not until reflection is added. If you tell us what this is for, we might be able to suggest some other solution... – HolyBlackCat Jul 03 '21 at 18:03
  • OK, thx. I guess waiting for Reflectios TS to become part of the standard might take too long. So I updated the original question with details on the problem for which I need the tuple of types. – A. Bigerl Jul 03 '21 at 19:26
  • What is a "virtual constructor"? – HolyBlackCat Jul 03 '21 at 19:34
  • Good question. I guess that is non-sense. What I wanted to state is that MyInt and MyDouble cannot have virtual methods. – A. Bigerl Jul 03 '21 at 19:41

1 Answers1

1

Getting "all complete types that satisfy a certain concept" is impossible (at least before we get reflection), so I'm answering the second part of the question.


Step 1: You might want to use something like this to get type names, this way you don't need the name variable in classes.

Step 2: Make a CRTP base, and inherit your classes from it:

template <typename T> struct Base {};

struct MyInt : Base<MyInt> {};
struct MyDouble : Base<MyDouble> {};

Step 3: Inject class registration code into the base:

template <typename T>
class Base
{
    static std::nullptr_t Register()
    {
        // This function will run for each derived class, when the program starts.

        std::ios_base::Init init; // Without this, `std::cout` might not work this early.
        std::cout << "Hello, world!\n";
        return nullptr;
    }

    inline static const std::nullptr_t dummy = Register();

    // Force `dummy` to be instantiated, even though it's unused.
    static constexpr std::integral_constant<decltype(&dummy), &dummy> dummy_helper{};
};

This code will run when your program starts, before entering main, for each class derived from Base<...>.

Step 4: Create a singleton, something like std::map<std::string, std::any(*)()>, to store functions to construct each class. Populate it from a static variable initializer in class Base, as shown above.

Make sure the singleton is a static variable in a function, not a global variable. Otherwise you'll experience the static init order fiasco.

HolyBlackCat
  • 78,603
  • 9
  • 131
  • 207