c++templatesc++-conceptsif-constexprpolicy-based-design

Using if-constexpr and concepts to detect an instantiation of a specific enriched policy type


In the Modern C++ Design book by Andrei Alexandrescu, there is a section on enriched policies which shows a class that implements a policy can provide optional member functions for an enriched policy. If the user uses a basic policy and never calls the optional function, the member function never gets looked at by the compiler and the code compiles. If the user tries to call the optional function with a policy class that does not support the enriched operation(s), it generates a compiler error.

struct BasicPolicy
{
    static void foo() {}
};

struct EnrichedPolicy
{
    static void foo() {}
    static void extra() {}
};

template <typename Policy>
struct Implementer : public Policy
{
    static void do_foo()
    {
        Policy::foo();
    }

    static void do_extra()
    {
        Policy::extra();
    }
};

Implementer<BasicPolicy> impl;
impl.do_foo();
impl.do_extra(); //expected compiler error, BasicPolicy does not provide extra() function.

Implementer<EnrichedPolicy> enriched_impl;
enriched_impl.do_foo();
enriched_impl.do_extra(); //fine, EnrichedPolicy has extra() function

My problem is that I have a function that takes a policy host class as a template parameter, and I need to conditionally branch in the function body depending on which policy the Implementer uses. Essentially what I have is a variation of the Implementer class that exposes two optional functions depending on which enriched policy is used.

struct EnrichedPolicy
{
    static void foo() {}
    static void extra() {}
};

struct AnotherEnrichedPolicy
{
    static void foo() {}
    static void even_more() {}
};

template <typename Policy>
struct Implementer : public Policy
{
    static void do_foo() { Policy::foo(); } //Compulsory basic policy function
    static void do_extra() { Policy::extra(); } //Optional function for one enriched policy
    static void do_much_more() { Policy::even_more(); } //Optional function for a different enriched policy
};

template <typename Policy>
void use_impl(Implementer<Policy> i)
{
    if constexpr(requires { Implementer<Policy>::do_extra(); })
    {
        std::cout << "Enriched policy\n";
    }
    else if constexpr(requires { Implementer<Policy>::do_much_more(); })
    {
        std::cout << "Different enriched policy\n";
    }
    else
    {
        std::cout << "Basic policy\n";
    }
}

I found that this does not work because the do_extra() and do_much_more() are part of the class definition and thus the constraint is syntactically valid, even if it would fail to compile with a specific template parameter if actually called.

Implementer<BasicPolicy> impl;
Implementer<EnrichedPolicy> enriched_impl;
Implementer<AnotherEnrichedPolicy> another_enriched_impl;

use_impl(impl); //"Enriched policy"
use_impl(enriched_impl); //"Enriched policy"
use_impl(another_enriched_impl); //"Enriched policy"

impl.do_extra(); //Compile-time error, it is not an enriched policy

If I instead write the concept for Policy template parameter, it works

template <typename Policy>
void use_policy(Implementer<Policy> i)
{
    if constexpr(requires { Policy::extra(); })
    {
        std::cout << "Enriched policy\n";
    }
    else if constexpr(requires { Policy::even_more(); })
    {
        std::cout << "Different enriched policy\n";
    }
    else 
    {
        std::cout << "Basic policy\n";
    }
}

use_policy(impl); //"Basic policy"
use_policy(enriched_impl); //"Enriched policy"
use_policy(another_enriched_impl); //"Different enriched policy"

However I'm wary of using this approach - I feel like I'm very much testing the implementation of the underlying policy, rather than the interface of the Implementer class. If in the future the do_extra() or do_much_more() function implementations change, I would need to rewrite the policy constraints.

Is there some better way of achieving my original idea, i.e. getting the compiler to realise that with a particular template parameter T the constraint is ill-formed? I.e.

use_impl(impl); //Should print "Basic policy"

Solution

  • You can add requires clauses to member functions of class templates, so Implementer::do_extra can be restricted to policies with extra, etc.

    template <typename Policy>
    struct Implementer : public Policy
    {
        static void do_foo() { Policy::foo(); }
        static void do_extra() requires requires { Policy::extra(); } { Policy::extra(); } 
        static void do_much_more() requires requires { Policy::even_more(); } { Policy::even_more(); }
    };
    

    requires requires { Policy::extra(); } { Policy::extra(); } does look somewhat repetitive, so you might want to name concepts for that

    template <typename Policy>
    concept has_extra = requires { Policy::extra(); };
    
    template <typename Policy>
    concept has_even_more = requires { Policy::even_more(); };
    
    template <typename Policy>
    struct Implementer : public Policy
    {
        static void do_foo() { Policy::foo(); }
        static void do_extra() requires has_extra<Policy> { Policy::extra(); } 
        static void do_much_more() requires has_even_more<Policy> { Policy::even_more(); }
    };