第六章 移动语义与enable_if<>

完美转发

根据语言规则,移动语义无法传递。移动语义不自动传递的原因在于确保原对象(被移动的对象)的状态合理。如果移动操作会自动传播到所有相关成员或其他关联的对象,那么可能会在无意间将某个对象移动多次,导致其内部值或资源丢失。因此,每次移动操作都需要明确地调用移动构造函数或移动赋值运算符,并且在完成移动后,原对象应处于一种有效的状态,通常是“空”或者“不可用”。

通常,对于参数的转发,需要实现三种情况:

#include <iostream>

class Base{
};
void g(Base& a) {std::cout<<"Call g(Base&)"<<std::endl;}
void g(const Base& a) {std::cout<<"Call g(const Base&)"<<std::endl;}
void g(Base&& a) {std::cout<<"Call g(Base&&)"<<std::endl;}


void f(Base& a) {g(a);} // 调用 g(Base&)
void f(const Base& a) {g(a);} // 调用 g(const Base&)
// void f(Base&& a) {g(a);} // 右值引用不自动传递,所以在f(Base{})时,创建的临时对象只会调用g(Base&)
void f(Base&& a) {
    // 虽然传进来的是右值,但是变量a的值类别是非常量左值
    // 所以如果不显式调用std::move(), 则会调用g(Base&)
    g(std::move(a));
}

int main() {
    Base a;
    const Base const_a;
    f(a); // f(Base&) -> g(Base&)
    f(const_a); // f(const Base&) -> g(const Base&)
    f(Base{}); // f(Base&&) -> g(Base&&)
    f(std::move(a)); // f(Base&&) -> g(Base&&)
}

那么对于模板函数,如何合并这三种操作呢?简单尝试一下:

#include <iostream>

class Base{
};
void g(Base& a) {std::cout<<"Call g(Base&)"<<std::endl;}
void g(const Base& a) {std::cout<<"Call g(const Base&)"<<std::endl;}
void g(Base&& a) {std::cout<<"Call g(Base&&)"<<std::endl;}

template<typename T>
void f(T& a) {
    g(a);
}
int main() {
    Base a;
    const Base const_a;
    f<Base>(a); // f(Base&) -> g(Base&)
    f<const Base>(const_a); // f(const Base&) -> g(const Base&)
    // f<Base&&>(Base{}); // f(Base&&) -> g(Base&&)
    // f<Base&&>(std::move(a)); // f(Base&&) -> g(Base&&)
}

可以发现,对于Base& 和 const Base&, 函数模板f完成了转发,但是对于右值版本,f无法匹配。或者,可以将T& 修改成T,再试试:

class Base{
};
void g(Base& a) {std::cout<<"Call g(Base&)"<<std::endl;}
void g(const Base& a) {std::cout<<"Call g(const Base&)"<<std::endl;}
void g(Base&& a) {std::cout<<"Call g(Base&&)"<<std::endl;}

// 对于没有引用的版本,因为无法显示调用 std::move,在右值传递时候调用的是g(Base&)
template<typename T>
void f(T a) {
    g(a);
}
int main() {
    Base a;
    const Base const_a;
    f<Base>(a); // f(Base&) -> g(Base&)
    f<const Base>(const_a); // f(const Base&) -> g(const Base&)
    f<Base&&>(Base{}); // f(Base&) -> g(Base&)
    f<Base&&>(std::move(a)); // f(Base&) -> g(Base&)
}

可以发现,现在f<Base&&>(Base{});至少能编译通过了,但是由于函数模板f没有显示调用std::move(), 所以移动语义无法传递,最终只能调用g(Base&)。

C++11 引入了std::forward,使得参数可以进行完美转发:


#include <iostream>

class Base{
};
void g(Base& a) {std::cout<<"Call g(Base&)"<<std::endl;}
void g(const Base& a) {std::cout<<"Call g(const Base&)"<<std::endl;}
void g(Base&& a) {std::cout<<"Call g(Base&&)"<<std::endl;}

// T&&:万能引用
template<typename T>
void f(T&& a) {
    g(std::forward<T>(a)); // 完美转发
}
int main() {
    Base a;
    const Base const_a;
    f(a); //  g(Base&)
    f(const_a); //  g(const Base&)
    f(Base{}); //  g(Base&&)
    f(std::move(a)); //  g(Base&&)
}

使用 enable_if<> 禁用模板

C++11开始,提供了辅助模板std::enable_if<> ,以在特定的编译时条件下忽略函数模板:

#include <iostream>

// typename std::enable_if<(sizeof(T) > 4)>::type 类型作为返回值
//  SFINAE 的模板特性:替换失败不为过
// 若表达式结果为 true,其类型成员类型将产生一个类型
// 若没有传递第二个模板参数,则该类型为 void
// 否则,该类型就是第二个模板参数类型。
// 若表达式结果为 false,则没有定义成员类型。
template<typename T>
typename std::enable_if<(sizeof(T) > 4)>::type
foo() {
    std::cout<<sizeof(T)<<std::endl;
}

int main() {
    // foo<int>(); // 编译错误,没有与其匹配的foo函数模板
    foo<double>(); // 编译成功 8 字节
    // foo<char>(); // 编译错误,没有与其匹配的foo函数模板
    foo<std::string>(); // 编译成功 32 字节
    return 0;
}
#include <iostream>

// 传入参数的版本, 如果条件为true,则返回类型为T
template<typename T>
typename std::enable_if<(sizeof(T) > 4), T>::type
foo() {
    return T{};
}

int main() {
    // foo<int>(); // 编译错误,没有与其匹配的foo函数模板
    auto a = foo<double>(); // 编译成功 8 字节
    std::cout<<a<<sizeof(a)<<std::endl;
    // foo<char>(); // 编译错误,没有与其匹配的foo函数模板
    auto b = foo<std::string>(); // 编译成功 32 字节
    std::cout<<sizeof(b)<<std::endl;
    return 0;
}

如果是C++17版本,std::enable_if<>::type 可以简写成 std::enable_if_t<>

上述写法并不直观,所以通常可以写成带默认值的函数模板参数:

#include <iostream>

// 传入参数的版本, 如果条件为true,则返回类型为T
// 如果bool值为true,则等价为:template<typename T, typename = void>
template<typename T, 
    typename = std::enable_if_t<(sizeof(T) > 4)>>
void foo() {
    std::cout<<sizeof(T)<<std::endl;
}
int main() {
    // foo<int>(); // 编译错误,没有与其匹配的foo函数模板
    foo<double>(); // 编译成功 8 字节
    // foo<char>(); // 编译错误,没有与其匹配的foo函数模板
    foo<std::string>(); // 编译成功 32 字节
    return 0;
}

上述写法可能还是不太直观,那么还可以使用using来设置别名,使得需求更明确:

#include <iostream>

// 重命名一个带条件的模板类型
template<typename T>
using EnableIfSizeGreater4 = std::enable_if_t<(sizeof(T) > 4)>;

template<typename T, typename = EnableIfSizeGreater4<T>>
void foo() {
    std::cout<<sizeof(T)<<std::endl;
}
int main() {
    // foo<int>(); // 编译错误,没有与其匹配的foo函数模板
    foo<double>(); // 编译成功 8 字节
    // foo<char>(); // 编译错误,没有与其匹配的foo函数模板
    foo<std::string>(); // 编译成功 32 字节
    return 0;
}

同时,新标准还引入了一个条件判断 std::is_convertible<FROM,TO>, 返回一个bool类型,表示类型FROM 是否可以强制转换为类型TO,结合使用样例为:


#include <iostream>
#include <type_traits>

// 重命名一个带条件的模板类型
template<typename T>
using EnableDouble = std::enable_if_t<(std::is_convertible<T, double>::value)>;

template<typename T, typename = EnableDouble<T>>
void foo() {
    std::cout<<"This type can be converted to double."<<std::endl;
}
int main() {
    foo<int>(); // int 可以隐式可转换成double
    foo<double>(); // double本身可以使用该模板
    foo<float>(); // 同理,float可以隐式可转换成double 
    // foo<std::string>(); // std::string 不能隐式可转换成double,所以不会生成匹配的模板函数
    return 0;
}

当然,C++20 提供了concept, 来进一步简化该语法:

#include <iostream>
#include <type_traits>

// 重命名一个带条件的模板类型
template<typename T>
concept EnableDouble = std::is_convertible<T, double>::value;

template<EnableDouble T>
void foo() {
    std::cout<<"This type can be converted to double."<<std::endl;
}
int main() {
    foo<int>(); // int 可以隐式可转换成double
    foo<double>(); // double本身可以使用该模板
    foo<float>(); // 同理,float可以隐式可转换成double 
    // foo<std::string>(); // std::string 不能隐式可转换成double,所以不会生成匹配的模板函数
    return 0;
}

引入concept,可以大大增加代码的可读性,语义上也更贴近人的理解。

总结

• 模板中,通过将参数声明为转发引用 (声明为模板参数名称后跟 && 形成的类型) 并在转发调 用中使用 std::forward<>(),就可以“完美”地转发参数了。
• 使用完美转发成员函数模板时,可能会比预定义用于复制或移动对象的特殊成员函数更匹配。
• 使用 std::enable_if<>,可以在编译时条件为 false 时禁用函数模板 (当条件确定,将忽略模板)。
• 概念允许对函数模板需求使用更直观的语法。

  • 24
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值