了解最新技术文章
...并使用 lambda 代替?
也就是说,而不是:
int sum(int a, int b) {
return a + b;}
你会写:
constexpr auto sum = [](int a, int b) -> int {
return a + b;};
听我说。
(无意使用)依赖于参数的查找被认为是错误。例如,通用代码中的不合格调用可能会调用在其参数的关联命名空间之一中找到的任意函数,而不是您打算在自己的命名空间中调用的函数!
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!
}}
因此,您需要限定通用代码中的所有调用!
在 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 不能重载。因此,我们需要使用不同的东西,而不是函数重载。这其实是一个优点!
过载装置有闭式和开式两种。封闭重载集是一个重载函数的作者知道所有其他重载的重载集。也就是说,N
我们希望以稍微不同的方式支持一组类型。这可以使用“重载”技巧轻松完成:
constexpr auto foo = tc::make_overload(
[](int i) { … },
[](float f) { … },
[](std::string const& str) { … });
或者,您可以编写一个通用 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。它可能看起来像这样:
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
…
}}
(通过更多的工作,您还可以重新使用名称“swap”作为专业化函数。)
现在用户只需调用合格的版本,它就会在内部进行相应的调度。至关重要的是,swap
它不再是一个重载集,并且可以编写为单个 lambda — 只需swap_impl
是一个函数,因为我们明确需要 ADL。
因此缺乏对 lambda 的重载支持实际上并不是问题。事实上,我们被迫使用更具表现力的方式来编写重载。好处是我们可以将整个重载集作为单个对象传递给其他函数!对于普通函数,我们需要直接使用“重载”习惯用法,或者使用像我们这样的宏tc_fn
,它将重载集提升到 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,我们被迫明确区分。推断模板参数只是模板参数或auto
lambda 参数,但显式模板参数将我们的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>
.
constexpr
与常规函数不同,编译器将隐式调用 lambda 的调用运算符constexpr
。无需考虑它并明确将您的函数标记为constexpr
,它就只是constexpr
。
需要我多说?
constexpr
请注意,示例中的首字母是指保存 lambda 对象的变量,而不是调用运算符。由于 lambda 不捕获任何内容,因此无论 lambda 主体中的代码如何,它们始终可以在编译时构造。
总而言之,放弃函数而转而使用命名 lambda 具有以下优点:
无法通过 ADL 找到它们。
它们是单个对象,而不是重载集。
它们允许区分隐式和显式模板参数。
他们是含蓄的constexpr
。
当然,也有缺点:
lambda 不能向前声明,必须在标头中定义。对于通用 lambda 来说这不是问题,并且模块的使用限制了编译时间的影响。尽管如此,这意味着间接递归可能无法直接使用该习惯用法来表达。
函数的符号名称变得丑陋:它现在是带有编译器合成名称的某个 lambda 的调用运算符,而不再是命名函数。
有点奇怪。
尽管如此,标准库已经开始使用这个习惯用法:范围定制点std::ranges::size
是内部使用 ADL 的函数对象,其中的算法std::ranges
也是所有函数对象,等等。
这意味着编译器编写者有动力去解决工程问题。
那么也许我们应该停止编写函数?
(从语言设计的角度来看,这是令人悲伤的,但我猜这就是 C++。)
24小时免费咨询
请输入您的联系电话,座机请加区号