c++定制点对象模式,不同命名空间中的参数和定制

laawzig2  于 2023-03-20  发布在  其他
关注(0)|答案(1)|浏览(131)

我最近学习了定制点对象模式,并尝试实现它。起初,它看起来像是一个很好的方法来创建一些基本功能并在不同类型上扩展它。
在下面的例子中,我使用Visual Studio 2022,并启用了c++20。
最后我得到了这个代码,和这个类似,我用std::ranges::swap作为一个参考。

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{};
}
    • 按预期工作**时的用法示例。
  • 全局命名空间中定义的结构和自定义函数
/* 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;
}
  • bar::main放入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;
}

但有一些例子,一个不能完全理解,为什么他们不工作

  • 结构和定制定义在不同的命名空间。定制函数可以清楚地看到定义,但当通过功能接口函数feature::apply访问时,我得到错误。
/* 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;
}
  • 为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;
}

也许我在第一个不起作用的例子中遗漏了一些作用域解析规则,但即使这样也不能解释为什么它不能与具有任何名称空间组合的内置类型一起工作。std::ranges::swap做同样的事情。这意味着,例如,如果我需要为某个类型添加定制,我需要将它放在定义该类的同一个名称空间中。
假设,在标准库中没有swap(std::string&, std::string&),或者由于某种原因,我需要替换它,我应该这样做。

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;
}

我觉得不对劲。
最初我认为函数use_feature的查找会延迟到feature::apply调用,因为feature::apply::operator()是一个函数模板,并且在这个函数中调用use_feature使用了模板参数。这看起来像是一个简单而灵活的方法来扩展不同类型的功能。但是我实现了它,尝试在不同的命名空间中移动部件,并尝试使用不同的类型...
在我看来,定制函数use_feature应该在当前或更高的名称空间中查找是合乎逻辑的。

iq0todco

iq0todco1#

第一个未编译的例子失败了,因为use_feature()应该被ADL找到。然而,这要求函数声明在与其参数相同的命名空间中。您的use_feature(Foo&)声明在一个 nested 命名空间bar中,因此ADL不考虑它。
第二个示例失败,因为ADL不适用于基本类型,因此重载解析在fn::operator()中找到的函数是已删除的use_feature函数模板。
可以通过在名称空间_feature_impl中声明use_feature(int&)来解决此问题
最初我认为函数use_feature的查找将延迟到功能::apply调用,因为feature::apply::operator()是一个函数模板,并且在此函数中调用use_feature使用了模板化参数。
这是正确的,最后两个示例失败,因为删除的use_feature函数模板仍然是重载解析期间找到的最佳候选模板。
还要仔细阅读关于cppreference的ADL规则,它们相当复杂,但确实回答了你的问题。
假设在标准库中没有swap(std::string&,std::string&)或者我出于某种原因需要替换它,我应该这样做。

**不。**即使这不是你所要求的,这也是一个误解。除非另有说明,否则向命名空间std添加成员或重载其中的函数都是未定义的行为。简而言之,你只能专门化依赖于至少一个用户定义类型的类模板。从C++20开始,专门化函数模板总是UB。

相关问题