2

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.

  • Struct and customization function defined in global namespace
/* 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;
}
  • Same as before, but customization is not defined
/* 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;
}
  • Same as the first example, but struct, customization function and usage of the feature are inside of namespace bar
/* 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;
}
  • Putting bar::main in bar::baz::main
/* 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.

  • Struct and customization defined in different namespace. Customization function can clearly see the definition, but when accessing though feature interface function feature::apply, I'm getting error.
/* 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;
}
  • Defining customization function for built-in types like int.
/* 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.

smidimir
  • 23
  • 4

1 Answers1

1

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.

chrysante
  • 2,328
  • 4
  • 24
  • Thank you for the answer and for the lead. I will look into ADL rules. I'm not sure how I feel about restriction that argument and function should be in the same namespace. I guess this is what was bothering me the most. I'm sure there was a reason to do this, but it feel counter intuitive if take into account example first not working example, when function by itself works, but though ADL don't. – smidimir Mar 18 '23 at 13:13
  • I know about std. Example with `std::range::swap` was meant to illustrate 2 points. 1. std::range::swap is acting the same 2. in order to add standard type (std::string) using this customization point, the function should be in the same namespace as std::string. – smidimir Mar 18 '23 at 13:15
  • Based on what You said, I just need add a different method or workaround to add customizations for fundamental types and types from standard library. – smidimir Mar 18 '23 at 13:17
  • 1
    To add customizations for fundamental types and types from `std`, you need to declare them in namespace `feature::_feature_impl`. This should however not be an issue since you control the API. These customization points are usually meant for users of your library to add special behaviour for _their_ types. – chrysante Mar 18 '23 at 13:18