技术文章

了解最新技术文章

当前位置:首页>技术文章>技术文章
全部 146 常见问题 7 技术文章 139

think cell博客:我们应该停止编写函数吗?

时间:2023-11-09   访问量:1036  标签: think cell,think-cell,函数

...并使用 lambda 代替?

也就是说,而不是:

int sum(int a, int b) {
    return a + b;}
一个功能

你会写:

constexpr auto sum = [](int a, int b) -> int {
    return a + b;};
一个命名的 lambda

听我说。

Lambda 不允许 ADL

(无意使用)依赖于参数的查找被认为是错误。例如,通用代码中的不合格调用可能会调用在其参数的关联命名空间之一中找到的任意函数,而不是您打算在自己的命名空间中调用的函数!

namespace my {
    template <typename T>
    void destroy(T* elem) {
        elem->~T();
        delete elem;
    }
    template <typename Rng>
    void destroy_range(Rng&& rng) {
        for (auto&& elem : rng)
          destroy(elem); // unintentional ADL!
    }}
如果用户提供更专门的重载,则可以触发 ADL 的不合格调用。单击播放按钮查看示例。

因此,您需要限定通用代码中的所有调用!

think-cell,我们特别注意通过强制用户使用 来限定所有函数,以防止无意的 ADL tc::这是通过将所有类型定义移动到单独的命名空间中来完成的:

namespace tc {
    namespace no_adl {
        struct foo {};
			
        // no functions here
    }
    using no_adl::foo;
    void use_foo(foo f);}
日常生活活动障碍

ADL只会考虑命名空间中的函数tc::no_adl,但所有函数都tc直接在其中。use_foo所以你不能通过ADL打电话;你必须限定它。

然而,如果我们只使用命名 lambda,我们就完全避免了这个问题:constexpr无法通过 ADL 找到变量。

namespace tc {
    struct foo {};
	
    constexpr auto use_foo = [](foo f) { … };}
Lambda 使得 ADL 屏障变得不必要

Lambda 不允许重载

常规函数可以重载,但 lambda 不能重载。因此,我们需要使用不同的东西,而不是函数重载。这其实是一个优点!

过载装置有闭式开式两种。封闭重载集是一个重载函数的作者知道所有其他重载的重载集。也就是说,N我们希望以稍微不同的方式支持一组类型。这可以使用“重载”技巧轻松完成

constexpr auto foo = tc::make_overload(
    [](int i) { … },
    [](float f) { … },
    [](std::string const& str) { … });
“重载”技巧允许封闭的 lambda 重载集

或者,您可以编写一个通用 lambda 来使用if constexpr和/或requires在类型之间分派:

constexpr auto foo = []<typename T>(T const& arg) {
    if constexpr (std::same_as<T, int>) {
        …
    } else if constexpr (std::same_as<T, float>) {
        …
    } else if constexpr (requires (T const& arg) { arg.c_str(); }) {
        …
    } else {
        static_assert(error<T>, "no matching overload for T");
    }};
手动实施重载决议

无论如何,我发现第二种方法比重载集干净得多:您可以完全控制重载的优先级和转换,不需要记住复杂的解析规则,并且如果某些内容不匹配,可以给出更好的错误消息。

第二种过载装置是开放式过载装置。这里我们要写一个定制点:用户可以针对自己的类型重载该函数。这也是 ADL 的唯一用例。

namespace std {
    template <typename T>
    void swap(T& lhs, T& rhs); // default
    template <typename T>
    void swap(std::vector<T>& lhs, std::vector<T>& rhs); // customization}namespace other {
    void swap(foo& lhs, foo& rhs); // customization}namespace my {
    template <typename T>
    void use(T& arg) {
        …
        using std::swap;  // enable default
        swap(arg, other); // allow ADL
        …
    }}
经典的基于 ADL 的定制点习惯用法

但是,使用总是有点尴尬,因为您必须从命名空间显式引入默认版本。如果将其分成两个函数,效果会好得多:一个可通过 ADL 进行自定义,另一个仅调用 ADL。它可能看起来像这样:

namespace std {
    template <typename T>
    void swap_impl(T& lhs, T& rhs); // default
    template <typename T>
        requires requires(T& lhs, T& rhs) { swap_impl(lhs, rhs); }
    void swap(T& lhs, T& rhs)  { // interface
        swap_impl(lhs, rhs); // enable ADL
    }
    template <typename T>
    void swap_impl(std::vector<T>& lhs, std::vector<T>& rhs); // customization}namespace other {
    void swap_impl(foo& lhs, foo& rhs); // customization}namespace my {
    template <typename T>
    void use(T& arg) {
        …
        std::swap(arg, other); // call interface
        …
    }}
一种改进的基于 ADL 的定制点习惯用法

(通过更多的工作,您还可以重新使用名称“swap”作为专业化函数。)

现在用户只需调用合格的版本,它就会在内部进行相应的调度。至关重要的是,swap它不再是一个重载集,并且可以编写为单个 lambda — 只需swap_impl是一个函数,因为我们明确需要 ADL。

因此缺乏对 lambda 的重载支持实际上并不是问题。事实上,我们被迫使用更具表现力的方式来编写重载。好处是我们可以将整个重载集作为单个对象传递给其他函数!对于普通函数,我们需要直接使用“重载”习惯用法,或者使用像我们这样的宏tc_fn,它将重载集提升到 lambda 中。

Lambda 使您可以控制模板参数

考虑一个像这样的函数std::make_unique

template <typename T, typename ... Args>std::unique_ptr<T> make_unique(Args&&... args);
std::make_unique作为一个函数

它有两种模板参数:第一种,T您需要在调用站点中显式指定。其次,Args由编译器推导。如果您不指定,这是一个错误,T因为它无法推断,如果您指定Args,这是一个坏主意,因为您可能会得到稍微错误的类型

我从来不喜欢两者之间没有区别两者都只是常规模板参数。通过使用命名 lambda,我们被迫明确区分。推断模板参数只是模板参数或autolambda 参数,但显式模板参数将我们的constexpr变量转换为constexpr模板:

template <typename T>constexpr auto make_unique = []<typename ... Args>(Args&&... args) {
    …};
std::make_unique作为命名 lambda

我们仍然被迫指定T但不能再指定Args(除非你做类似的事情make_unique<int>.operator()<int>(0),但谁做的?!)。这是一个很好的区别。

此外,我们可以make_unique<int>作为单个可调用对象自由传递,该对象接受任意参数并返回std::unique_ptr<int>.

Lambda 是隐式的constexpr

与常规函数不同,编译器将隐式调用 lambda 的调用运算符constexpr无需考虑它并明确将您的函数标记为constexpr,它就只是constexpr

需要我多说?

constexpr请注意,示例中的首字母是指保存 lambda 对象的变量,而不是调用运算符。由于 lambda 不捕获任何内容,因此无论 lambda 主体中的代码如何,它们始终可以在编译时构造。

结论

总而言之,放弃函数而转而使用命名 lambda 具有以下优点:

当然,也有缺点:

尽管如此,标准库已经开始使用这个习惯用法:范围定制点std::ranges::size是内部使用 ADL 的函数对象,其中的算法std::ranges也是所有函数对象,等等。

这意味着编译器编写者有动力去解决工程问题。

那么也许我们应该停止编写函数?

(从语言设计的角度来看,这是令人悲伤的,但我猜这就是 C++。)


上一篇:think cell中新的 static constexpr std::integral_constant 习惯用法

下一篇:think cell博客:事件或无事件?

发表评论:

评论记录:

未查询到任何数据!

免费通话

24小时免费咨询

请输入您的联系电话,座机请加区号

免费通话

微信扫一扫

微信联系
返回顶部