c++namespacescustomizationname-lookupcustomization-point

c++ customization point object pattern. Argument and customization in different namespaces


I recently learned about customization point object pattern and tried implementing it. At first, it looked like a very good way to make some base functionality and extend it on different types.

In examples below I use Visual Studio 2022 with c++20 enabled.

I ended up with this code, similar to this. I used std::ranges::swap as a reference.

namespace feature {
    namespace _feature_impl {
        /* to eliminate lookup for wrong functions */
        template<typename T>
        void use_feature(T&) = delete;

        /* filtering customizations */
        template<typename T>
        concept customization =
            requires(T& reward) {
                { use_feature(reward) };
            };

        struct fn {
            /* allow only if there is customization */
            /* compile-time error otherwise */
            constexpr void operator () (customization auto& reward) const {
                use_feature(reward);
            }
        };
    }

    /* main interface to access feature */
    inline constexpr auto apply = _feature_impl::fn{};
}

Examples of usage when it works as expected.

/* result: compiles, prints "Foo" */
struct Foo {};

void use_feature(Foo&) {
    std::cout << "Foo\n";
}

auto main(int argc, char const** argv) -> int {
    auto foo = Foo{};
    feature::apply(foo);
    
    return 0;
}
/* result: doesn't compile */
struct Foo {};
struct Bar {};

void use_feature(Foo&) {
    std::cout << "Foo\n";
}

auto main(int argc, char const** argv) -> int {
    auto bar = Bar{};
    /* passing an object of type, that is not supported */
    feature::apply(bar);

    return 0;
}
/* result: compiles, prints "Foo" */
namespace bar {
    struct Foo {};

    void use_feature(Foo&) {
        std::cout << "Foo\n";
    }

    void main() {
        auto foo = Foo{};
        feature::apply(foo);
    }
}

auto main(int argc, char const** argv) -> int {
    bar::main();

    return 0;
}
/* result: compiles, prints "Foo" */
namespace bar {
    struct Foo {};

    void use_feature(Foo&) {
        std::cout << "Foo\n";
    }

    namespace baz {
        /* now usage in nested namespace baz */
        void main() {
            auto foo = Foo{};
            feature::apply(foo);
        }
    }
}

auto main(int argc, char const** argv) -> int {
    bar::baz::main();

    return 0;
}

But there are some examples that a can't quite understand, why they don't work.

/* result: doesn't compiles */
/* Error C3889 - call to object of class type 'feature::_feature_impl::fn': no matching call operator found */
struct Foo {};
namespace bar {
    void use_feature(Foo&) {
        std::cout << "Foo\n";
    }

    void main() {
        auto foo = Foo{};
        feature::apply(foo);
    }
}

auto main(int argc, char const** argv) -> int {
    bar::main();

    return 0;
}
/* result: doesn't compiles */
/* Error C3889 - call to object of class type 'feature::_feature_impl::fn': no matching call operator found */
void use_feature(int&) {
    std::cout << "Foo\n";
}

auto main(int argc, char const** argv) -> int {
    auto i = int{0};
    feature::apply(i);

    return 0;
}

May be I'm missing some scope resolution rules with the first not working example, but even that doesn't explain why it doesn't work with built-in types with any combinations of namespaces. std::ranges::swap does the same things. It means if, for example, I need to add customization for some type, I need to place it in the same namespace where this class is defined.

Assuming, that in standard library there is no swap(std::string&, std::string&) or I, for some reason, need to replace it, I should do something like this.

namespace std {
    void swap(std::string&, std::string&) {
        std::cout << "Foo\n";
    }
}

auto main(int argc, char const** argv) -> int {
    auto s = std::string{};
    std::ranges::swap(s, s);

    return 0;
}

It doesn't feel right to me.

Initially I thought that lookup for function use_feature will be delayed until feature::apply call, because feature::apply::operator() was a function template, and call to use_feature inside this function used templated argument. It looked like an easy and flexible way to extent functionality on different types. But than I implemented it, tried to move around parts in different namespaces and tried to use with different types...

It seemed logical to me that customization functions use_feature would be looked up in current namespace or higher.


Solution

  • The first non compiling example fails, because use_feature() is meant to found by ADL. That however requires the function to be declared in the same namespace as its argument. Your use_feature(Foo&) is declared in a nested namespace bar and is thus not considered by ADL.

    The second example fails because ADL does not apply to fundamental types so the function that is found by overload resolution in fn::operator() is the deleted use_feature function template.

    You can solve this by declaring use_feature(int&) in namespace _feature_impl

    Initially I thought that lookup for function use_feature will be delayed until feature::apply call, because feature::apply::operator() was a function template, and call to use_feature inside this function used templated argument.

    This is correct, the last two examples fail because the deleted use_feature function template is still the best candidate found during overload resolution.

    Also carefully read the rules of ADL on cppreference. They are quite complex but they do answer your question.

    Assuming, that in standard library there is no swap(std::string&, std::string&) or I, for some reason, need to replace it, I should do something like this.

    No. Even if this is not what you are asking for, this is a misconception. It is undefined behaviour to add members to namespace std or to overload functions in it, unless otherwise specified. In short you may only specialize class templates that depend on at least one user defined type. From C++20 on it is always UB to specialize function templates.