当前位置: 首页 > news >正文

C++模板:编译时模拟Duck Typing

 
C++泛型与多态(4): Duck Typing - 简书

 

James Whitcomb Riley在描述这种is-a的哲学时,使用了所谓的鸭子测试Duck Test):

当我看到一只鸟走路像鸭子,游泳像鸭子,叫声像鸭子,那我就把它叫做鸭子。(When I see a bird that walks like a duck and swims like a duck and quacks like a duck, I call that bird a duck.)

鸭子测试

鸭子测试

Duck Typing不是动态语言的专利。C++作为一门强类型的静态语言,也对此特性有着强有力的支持。只不过,这种支持不是运行时,而是编译时。

其实现的方式为:一个模板类或模版函数,会要求其实例化的类型必须具备某种特征,如某个函数签名,某个类型定义,某个成员变量等等。如果特征不具备,编译器会报错。

通过之前的解释我们不难发现,Duck Typing要表达的多态语义如下图所示:

DuckTyping的语义

DuckTyping的语义

适配器:类型萃取

Duck Typing需要实例化的类型具备一致的特征,而模板特化的作用正是为了让不同类型具有统一的特征(统一的操作界面),所以模板特化可以作为Duck Typing与实例化类型之间的适配器。这种模板特化手段称为萃取Traits),其中类型萃取最为常见,毕竟类型是模板元编程的核心元素。

所以,类型萃取首先是一种非侵入性的中间层。否则,这些特征就必须被实例化类型提供,而就意味着,当一个实例化类型需要复用多个Duck Typing模板时,就需要迎合多种特征,从而让自己经常被修改,并逐渐变得庞大和难以理解。

Type Traits的语义

Type Traits的语义

另外,一个Duck Typing模板,比如一个通用算法,需要实例化类型提供一些特征时,如果一个类型是类,则是一件很容易的事情,因为你可以在一个类里定义任何需要的特征。但如果一个基本类型也想复用此通用算法,由于基本类型无法靠自己提供算法所需要的特征,就必须借助于类型萃取

结论

这四篇文章所介绍的,就是C++泛型编程的全部关键知识。

从中可以看出,泛型是一种多态技术。而多态的核心目的是为了消除重复隔离变化,提高系统的正交性。因而,泛型编程不仅不应该被看做奇技淫巧,而是任何一个追求高效的C++工程师都应该掌握的技术。

同时,我们也可以看出,相关的思想在其它范式和语言中(FP,动态语言)也都存在。因而,对于其它范式和语言的学习,也会有助于更加深刻的理解泛型,从而正确的使用范型。

最后给出关于泛型的缺点:

  1. 复杂模板的代码非常难以理解;
  2. 编译器关于模板的出错信息十分晦涩,尤其当模板存在嵌套时;
  3. 模板实例化会进行代码生成,重复信息会被多次生成,这可能会造成目标代码膨胀;
  4. 模板的编译可能非常耗时;
  5. 编译器对模板的复杂性往往会有自己限制,比如当使用递归时,当递归层次太深,编译器将无法编译;
  6. 不同编译器(包括不同版本)之间对于模板的支持程度不一,当存在移植性需求时,可能出现问题;
  7. 模板具有传染性,往往一处选择模板,很多地方也必须跟着使用模板,这会恶化之前的提到的所有问题。

这篇作者对此的原则是:在使用其它非泛型技术可以同等解决的前提下,就不会选择泛型。



作者:_袁英杰_
链接:https://www.jianshu.com/p/4939c934e160
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

template <typename T>
void f(T& object)
{object.f(0); // 要求类型 T 必须有一个可让此语句编译通过的函数。
}//换成下面版本,会出现编译错误。主要是C1和C4编不过
//template <typename T>
//void f(T& object)
//{
//    int result = object.f(0);
//    std::ignore = result;
//    // ... 
//}struct C1
{void f(int i){++i;std::cout << R"(f(int))" << std::endl;return ;}
};struct C2
{int f(char){std::cout << R"(f(char))" << std::endl;return 2;}
};struct C3
{int f(unsigned short, bool isValid = true){std::cout << R"(f(unsigned short, bool isValid = true))" << std::endl;return 3;}
};struct C4
{struct Object{};struct Foo{};Foo* f(Object*){std::cout << R"(f(Object*))" << std::endl;return NULL;}
};//void f(C1& object) 
//{
//    object.f(0); // 要求类型 T 必须有一个可让此语句编译通过的函数。
//}int main()
{C1 o1;C2 o2;C3 o3;C4 o4;f(o1);f(o2);f(o3);f(o4);return 1;
}输出:
f(int)
f(char)
f(unsigned short, bool isValid = true)
f(Object*)

 

Duck Typing with C++ templates

class foo
{
public:std::string as_string() const{return "i am foo";}
};class baz
{
public:// look ma, no 'as_string'
};template <typename T> class repr_type
{
public:repr_type(const T& o) : m_o(o){}std::string as_string() const{return call_as_string<T>(nullptr);}private:template <class C> std::string call_as_string(decltype(&C::as_string)) const{return m_o.as_string();}template <class C> std::string call_as_string(...) const{return "print pointer";//return string::format("%p", &m_o);}const T& m_o;
};template <typename T> 
std::string as_string(const T& o)
{return repr_type<T>(o).as_string();
}int main() 
{foo o;as_string(o);baz b;as_string(b);return 1;
}
对一个类进行增强class foo
{
public:std::string as_string() const{return "i am foo";}
};class baz
{
public:// look ma, no 'as_string'
};template <typename T> class repr_type
{
public:repr_type(const T& o) : m_o(o){}std::string as_string() const{return call_as_string<T>(nullptr);}private:template <class C> std::string call_as_string(decltype(&C::as_string)) const{return m_o.as_string();}template <class C> std::string call_as_string(...) const{return "print pointer";//return string::format("%p", &m_o);}const T& m_o;
};template <typename T> 
std::string as_string(const T& o)
{return repr_type<T>(o).as_string();
}class spreadsheet
{
public:void set(int x, int y, const char* text){//给表格中的一个单元设置内容std::cout << "hello" << std::endl;}//相当于以一种简介的接口对各种数据类型进行接收,没有as_string的就不做任何事情template <typename T> void set(int x, int y, const T& instance){set(x, y, as_string(instance).c_str());}
};int main() 
{spreadsheet s;foo obj;s.set(0, 0, obj); // where i could be "anything"return 1;
}

Duck Typing with C++ templates

So, folks, what is this Duck Typing thingy? Trusty wikipedia says: In duck typing, an object's suitability is determined by the presence of certain methods and properties (with appropriate meaning), rather than the actual type of the object. Let's take a look at a concrete example from a pet library of mine.

Often in code - especially often when logging stuff - you want to represent things as a string to be printed. A useful start is to use function overloading, mainly because there are some types of things that you cannot really extend. For example, you could have something like this:

    inline std::string as_string(const char* p){return p ? p : "<nullptr>";}inline std::string as_string(bool value){return value ? "true" : "false";}inline std::string as_string(int32_t number){return string::format("%d", number);}

The nice thing of this is: you can type as_string(x) and for many things, you will get out a useful string representation without having to care what the actual object is.

Enter objects. For many of these, the above approach will work as well:

    inline std::string as_string(const std::vector<std::string> v){return string::format("vector<string> with %d objects", (int) v.size());}inline std::string as_string(const my_elaborate_class& c){return string::format("my_elaborate_class at %p", &c);}

Now, maybe you want to have more information in as_string, and maybe you need to be able to access private data members of the objects. This can be solved by introducing a member function as_string, starting with this design:

    class foo{public:std::string as_string() const;};std::string as_string(const foo& x){return x.as_string();}class bar{public:std::string as_string() const;};std::string as_string(const bar& x){return x.as_string();}

This works only if you write separate as_string overloads for each class that implements as_string members, a rather pointless task. Nothing for us lazy programmer folk!

You could define an interface and force all objects to inherit it:

    interface can_be_represented_as_string{virtual std::string as_string() const = 0;};class foo : public can_be_represented_as_string{virtual std::string as_string() const override;};class bar : public can_be_represented_as_string{virtual std::string as_string() const override;};std::string as_string(const can_be_represented_as_string& something){return something.as_string();}

OK, nice! Now you can throw almost anything at as_string, and get a string back. This works, but it requires you to define inheritance on all your objects, which is an eyesore and anyway may sometimes not be feasible, because you may have objects that you don't have control over (or you already have a large hierarchy of objects and people would complain if you'd start messing up their inheritances). Enter duck typing

As written at the beginning, in duck typing, an object's suitability is determined by the presence of certain methods and properties (with appropriate meaning), rather than the actual type of the object

Looking back where we started:

    class foo{public:std::string as_string() const;};class bar{public:std::string as_string() const;};

Both foo and bar are objects that have methods named as_string with appropriate meaning, but the types are different. What can we do about that? This:

    template <typename T> std::string as_string(const T& o){return o.as_string();}

So this is a generic method that works because there are methods with the proper meaning - not because the type has inherited something special. Nice!

OK, but now what about objects that do no implement a method as_string with appropriate meaning? It would be nice if we could say something like

  1. If there is a as_string overload, use that
  2. If the object has as_string(), use that
  3. Otherwise, default to just dumping the address of the object

At this point, the first two work, but the third one is causing a headache. It turns out that there is a SFINAE based idiom to detect if a type has a member but that is only half the solution: most examples just end up in a template function that can be used to detect the fact, but not really do something about it. To see the problem, let's assume you've followed the examples I've linked to and have a template has_as_string<type>::value. And you have the following setup:

    class foo{public:std::string as_string() const;};class baz{public:// look ma, no 'as_string'};...static_assert(has_as_string<foo>::type, "");static_assert(!has_as_string<baz>::type, "");...template <typename T> std::string as_string(const T& o){if(has_as_string<T>::value)return o.as_string();return string::format("%p", &o);}

The static-asserts will work (provided your implementation is correct), but the as_string template won't, because it will attempt to generate code that uses as_string even if the object doesn't have the method. So you need something more involved:

    template <typename T> class repr_type{public:repr_type(const T& o):m_o(o){}std::string as_string() const{return call_as_string<T>(nullptr);}private:template <class C> std::string call_as_string(decltype(&C::as_string)) const{return m_o.as_string();}template <class C> std::string call_as_string(...) const{return string::format("%p", &m_o);}const T& m_o;};template <typename T> std::string as_string(const T& o){return repr_type<T>(o).as_string();}

It is worth looking at the fine print on this one:

  • Based on whether the type passed has a method as_string, this template will print either the result of that function, or simply the objects address.
  • Note the template function call_as_string: the template resolver will prefer the more specific type, and it will be able to use the first template if the address of as_string can be taken.
  • Note that because this is a template function, it is not compiled until it is actually used: so for things that don't have as_string a call to as_string will never be issued.
  • Also note for this to work, I cannot also pass in the object instance: so I need a proxy object - repr_type template - to hold a reference to the object while letting SFINAE do its work

So what use is any of this?

Well, assume you have a set of as_string thingies as discussed here: functions using overloading, some template magic and so on. All is fine and well and one day you start writing a method that takes strings. For example, assume you have a spreadsheet and you want to fill it with text:

    class spreadsheet{public:void set(int x, int y, const char* text);};

Now for most of the things you have, you can write

    spreadsheet s;...s.set(0,0,as_string(i)); // where i could be "anything"

But wait, it gets better: let's enhance the spreadsheet class:

    class spreadsheet{public:void set(int x, int y, const char* text);template <typename T> void set(int x, int y, const T& instance){set(x,y, as_string(instance));}};

This will result in even more clean client code:

    spreadsheet s;...s.set(0,0,i); // where i could be "anything"

Personally, I like my client code as readable as possible. I know that can be hard, because C++ tries really hard to be ugly and uglier still, but sometimes after years disparate features like overloading and templates and improved rules in C++11 work together to make the code look simple. Nice!


http://www.mrgr.cn/news/81528.html

相关文章:

  • 外部化内部类
  • vue3----思维导图
  • 【ORB-SLAM3:相机针孔模型和相机K8模型】
  • Python 正则表达式全面解析
  • 无人直播源码
  • 如何通过运行时威胁洞察提升反欺诈策略
  • LLaMA-Factory GLM4-9B-CHAT LoRA 指令微调实战
  • 【Java 学习】深度剖析Java多态:从向上转型到向下转型,解锁动态绑定的奥秘,让代码更优雅灵活
  • 【stm32can】
  • CSharp: Oracle Stored Procedure query table
  • 重温设计模式--10、单例模式
  • STM32项目之环境空气质量检测系统软件设计
  • 【Git】-- 版本说明
  • DX12 快速教程(2) —— 渲染天蓝色窗口
  • 笔记本通过HDMI转VGA线连接戴尔显示器,wifi不可用或网速变慢
  • 大数据实验二
  • 鸿蒙之路的坑
  • soular使用教程
  • KylinOS V10 SP3下编译openGauss与dolphin插件
  • 操作系统导论读书笔记
  • 水库大坝三维模型的开发和使用3Dmax篇
  • 基于STM32F103控制L298N驱动两相四线步进电机
  • 数据库管理-第275期 Oracle 23ai:画了两张架构图(20241225)
  • idea配置gitee仓库
  • Flink调优----资源配置调优与状态及Checkpoint调优
  • FFmpeg 的常用API