一、应用中的问题
似乎上一篇的应用中,这个Demo虽然简单,但总算把门踢开了。但事实是这样子么,会不会被门槛绊一跤。这得用事实来说话。学习过c/c++的人都有一个很经验很经验的经验,那就是类型会被隐式转换,这个隐式转换是真方便,也是真坑人。那么这个隐式转换怎么控制?这才是一个重要的问题。举一个简单例子,有一个int型和一个short型做运算,哪个类型更能容纳数据结果,就会转换到哪个,也就是整型。但是,如果实际的场景的要求是必须转到short呢?这个现象在有符号和无符号的数据运算过程中就很容易出现问题。
好,说上面的这些问题,就是引出上文中的三类型统一后的推断的问题以及其会造成什么样的后果。从而以此为基础,引出如何解决这种问题,也就是前面提到的SFINAE和c++20中推出的概念(concepts).
二、解决方式及分析
1、引入
先看一个非常简单的问题,把仿函数内部的operator重载的const关键字去掉,再编译,就会出现下面的问题:
“具有类型“const F”的表达式会丢失一些 const-volatile 限定符以调用“void WorkTaskFunc::operator ()(void)”,继续向下看,就会发现一个提示“std::is_convertible<TaskImpl *,BaseTask *>”,看到std::is_convertible是否觉得很熟悉,在前面的concepts中有一个std::convertible_to(c++20),它简化了上面的那个std::is_convertible,说的清晰一些,就是这两个对象没法互相转化,代码有问题。这个问题很好解决,加上const就OK了。
2、更好的兼容性
在不同的编译器上,可能对一些仿函数和函数对象及函数指针上会有一些处理的不同。最显而易见的就是这就会导致另外一个问题,同样的代码可能Windows是编译OK的,但到Linux上就不行了。为了保证代码的可移植性,可以进一步对类型进行控制,如下面的代码:
class TaskWrapper
{
public:
TaskWrapper() = default;
template<typename F>
TaskWrapper(F&& f)
{
using type_decay = std::decay_t<F>;
using standType = TaskImpl<type_decay>;
//typedef TaskImpl<F> standType;
pTask_ = std::make_unique<standType>(std::forward<type_decay>(f));
}
~TaskWrapper() = default;
......
};
std::decay在前面分析过,在c++14中,定义了“template< class T > using decay_t = typename decay::type;”,通过它就可以让编译器退化相应的类型,保证类型被安全的识别。
3、不同类型的对象互相使用
是不是觉得上面的问题比较简单,其实还有更麻烦的。在前面的例子中,可不可以用一个TaskWrapper对象来生成另外一个TaskWrapper呢?类似下面的代码:
TaskWrapper tw1{ WorkTask };
TaskWrapper tw2{ tw1 };
在实际程序里编译一下,在VC和G++中,编译是没有问题的,运行也是没有问题的。但在程序中明明已经把复制构造函数给去除了。执行一下,也没有问题。这就需要更精准的处理一下这个问题,有没有想到什么,前边在分析concepts中的一些技术也提到过早期使用SFINAE,那么可以使用SFINAE来处理一下代码:
class TaskWrapper;//前向声明
template <typename F>
using is_ok_wrapper =
std::enable_if_t<!std::is_same_v< std::remove_cvref_t<F>, TaskWrapper >,int>;
class TaskWrapper
{
public:
//TaskWrapper() = default;
template<typename F, is_ok_wrapper<F> = 0>
TaskWrapper(F&& f)
{
using type_decay = std::decay_t<F>;
using standType = TaskImpl<type_decay>;
//typedef TaskImpl<F> standType;
pTask_ = std::make_unique<standType>(std::forward<type_decay>(f));
}
......
};
std::is_same_v< std::remove_cvref_t, TaskWrapper >,判断这两个类型是否一样,std::remove_cvref_t等于是把类型外包裹的CV限定符和引用去除,这个有点类似于decay,外面有一个非(!)的符号表示相反,而std::enable_if是指如果第一个值为True,则可以正确的使用第二个类型参数,否则无该成员 typedef 的类型,也即无法继续推导模板。把其应用到上面的工程中,继续编译,则会报错误:
““TaskWrapper::TaskWrapper(const TaskWrapper &)”: 尝试引用已删除的函数”,这样就达到目的了。
但是这个似乎还是有点不舒服,让开发者和维护者都不太有感觉。那么可以使用c++20中的concepts,写成下面的代码:
template <typename F>
concept is_ok_wrapper = !std::is_same_v<std::remove_cvref_t<F>, TaskWrapper>;
class TaskWrapper
{
public:
//TaskWrapper() = default;
//直接限定F
template<is_ok_wrapper F>
TaskWrapper(F&& f)
{
using type_decay = std::decay_t<F>;
using standType = TaskImpl<type_decay>;
pTask_ = std::make_unique<standType>(std::forward<type_decay>(f));
}
......
};
编译后的结果与上面的错误信息一致。但看上去要清爽不少,更容易为开发者理解。
三、总结
c++中的类型擦除的应用,基本上算完成了。从这个类型擦除从开始到结束的过程,可以可以清楚的把以前学到的知识进行一个横向的组织。在c++的文档中,每一个模板类每个示例都清楚,但如何用起来,一团迷糊,这两篇文章也算是给出一个应用的简单的例子。