Search code examples
c++templatesc++14template-argument-deductionctad

Pre-C++17 replacement for CTAD?


(Or: "How to store the return value of a template-argument-deduced function where auto is not allowed")

I'm working on a generic C++ framework made up of several class templates that allow to combine specializations of each other. For external reasons, the language version is restricted to C++14. Using my framework in non-generic user code ends up in complex template specializations like

MyType<foo<bar, baz, quuz<quz>, zip<zap<zoo>>> myObj{
    foo<bar, baz, quuz<quz>, zip<zap<zoo>>{
        bar{},
        baz{},
        quuz<quz>{
            quz{}
        },
        zap<zoo>{
            zoo{}
        }
    }
};

Of course, template parameter types do not have to equal the types of ctor arguments, but in the case of my present code, this happens quite often. The displayed code is only a caricature of the real code (many more types/templates), but it should be sufficient to point out my problem (Please let me know if you need some compilable example code in order to evaluate):

When I cite all the template arguments needed, this causes a lot of noise in the code, especially for those types that have to be named redundantly. Type aliases (using) do not look like the perfect remedy because the example would need lots of these, as well.

I inspected the possibilities of template argument deduction, and I learned that C++14 supports template argument deduction only with function templates (such as the often-discussed example of std::make_pair), but that class template argument deduction (CTAD) will not be available before C++17.

Using function-based template-argument deduction (and "make functions"), I can "simplify" the object initialization expression on the RHS of the example code above, which leads to something like

MyType<foo<bar, baz, quuz<quz>, zip<zap<zoo>>> myObj = make_obj( 
    make_foo(
        bar{},
        baz{},
        make_quuz(quz{}),
        make_zap(zoo{})
    )
);

In contexts where auto can be used, I can even reach my goal of brief & expressive code like

auto myObj = make_obj(make_foo(bar{}, baz{}, make_quuz(quz{}), make_zap(zoo{})));

but this seems to be possible only in certain contexts where, e.g., myObj is a local variable.

I would like to realize a framework library that enables users to create their own, non-generic classes based on my templates, but in a way where they do not have to repeat (and remember) all the types (= template specializations) used to realize their class implementation, like here:

class UserType
{
    public:
        UserType() : m_frameworkImplementation{make_foo(/*...*/)} {}
    private:
        // This is illegal code, of course. Only to show what I'd like to realize...
        auto m_frameworkImplementation;
};

Sadly, I'm not able to do anything with auto here, and I cannot replace auto with MyType if I don't copy all the template arguments to it.

I also made some futile attempts with decltype, lambdas and other stuff, but I didn't even gain a hint on a possible solutions (and it feels to embarrassing to show any of these failed attempts here). There doesn't seem to be a way to deduce the specialized type of a class template construct, or of the return value of the deduced specialization of a function template (e.g., "make function"), at a point where the deduced type is needed to instantiate the type of a member variable.

What did I overlook? Do I have to wait until I can use C++17 or C++20, to achieve the desired result?


Solution

  • CTAD is not going to help you with this, as the compiler needs to know the type of the members at the line it is parsing the member, unless the class is a template, and UserType is not a template.

    if the class must not be templated then you can use decltype with "in class initialization of members" to create a simpler interface for members as follows.

    #include <utility>
    
    auto make_foo()
    {
        return (int)5;
    }
    
    #define DEFINE_MEMBER(var_name, initializer) \
    decltype(initializer) var_name = initializer
    
    class UserType
    {
        public:
            UserType() {}
        private:
            DEFINE_MEMBER(m_frameworkImplementation, make_foo());
    };
    
    int main()
    {
        UserType type;
    }
    

    this compiles under -std=c++14

    you only need to provide a function that returns the "default construtor", you don't need to do it for other constructors.

    #include <utility>
    
    int make_foo()
    {
        return {};
    }
    
    #define DEFINE_MEMBER(var_name, initializer) \
    decltype(initializer) var_name = initializer
    
    class UserType
    {
        public:
            UserType(double val): m_frameworkImplementation{val} {}
        private:
            DEFINE_MEMBER(m_frameworkImplementation, make_foo());
    };
    
    int main()
    {
        UserType type(1);
    }