概念
多态(polymorphism),字面意思的解释是指一个 function or object 可以在不同状况下有不同的行为。多态从完成上大致分为静态多态(static polymorphism)和动态多态(dynamic polymorphism)两种。
以下三节首要参考:Polymorphism in C++ – cppdev 与 Performance of dynamic polymorphism – cppdev 两篇文章
动态多态(Dynamic Polymorphism)
动态指是真实履行的代码是在 运行时 才干确定的。 完成首要依靠于 virtual function 的 overriding。
example:
class Base
{
public:
int a;
virtual void doSomething()
{
std::cout << "doSomething from Base\n";
}
virtual void doOtherThing()
{
std::cout << "doOtherThing from Base\n";
}
};
class Derived : public Base
{
public:
int b, c;
void doSomething() override // override implies virtual
{
std::cout << "doSomething from Derived\n";
}
};
std::shared_ptr<Base> b{std::make_shared<Derived>()};
b->doSomething(); // prints "doSomething from Derived"
当一个 function 被 virtual 界说时,编译器会得知这个 function 或许会在子类中被重界说,因而只能在 runtime 时才干得到正确的履行代码。那么程序如何在运行时找到正确的 function?这儿就需求依靠 vtable 与 vptr 结构。
vtable 与 vptr
每个包含 virtual function 的 class 都会包含一个 vptr 用于指向一个 vtable 结构。vtable 用于存储一个 class 的一切 virtual function 的地址信息。
经过 -O0 得到的编译代码中,vtable 信息如下:
具体的编译成果可见:Compiler Explorer
静态多态(Static Polymorphism)
静态多态的静态是指 object 的类型以及要调用的 function 都是在编译器就决定好的。一般完成静态多态的办法首要包含:
- function overloading
- operator overloading
- templates
// function overloading
double do_oper(int a) // #1
{
return a * 2;
}
double do_oper(double a) // #2
{
return a * 2;
}
auto oper1 = do_oper(5); // #1 is called
auto oper2 = do_oper(5.0); // #2 is called
// operator overloading(already provided by compiler)
auto sum = 1 + 2; // operator+ adds the integers
auto concat = std::string{"1"} + std::string{"2"}; // operator+ concatenates the strings
auto sum_double = do_sum(1.0, 2.0); // double do_sum(double a, double b) is called
// template
template <class T>
T do_sum(T a, T b)
{
return a + b;
}
auto sum_int = do_sum(1, 2); // int do_sum(int a, int b) is called
此外,运用 template 完成静态多态还有一种专门的规划办法:CRTP(curiously recurring template pattern ) 其界说办法如下:
template<class Z>
class Y {};
class X : public Y<X> {};
一般的规划是在 base class 中,完成一个函数,将 base 强转为 T 对应的 class,调用其对应的 function
example:
#include <iostream>
template <class Derived>
struct Base { void name() { (static_cast<Derived*>(this))->impl(); } };
struct D1 : public Base<D1> { void impl() { std::cout << "D1::impl()\n"; } };
struct D2 : public Base<D2> { void impl() { std::cout << "D2::impl()\n"; } };
int main()
{
Base<D1> b1; b1.name();
Base<D2> b2; b2.name();
D1 d1; d1.name();
D2 d2; d2.name();
}
// output:
D1::impl()
D2::impl()
D1::impl()
D2::impl()
动态多态的功能问题
运行时才干确定真实履行的函数,会带来以下问题:
- 额定的存储空间(vptr)
- 额定的重定向 (pointer dereference)
- 无法 inline 化
- cache miss
额定的存储空间与重定向
比较 native 的函数调用,多了一步查表操作 例子:quick-bench.com/q/E9ZRucuYA… 因而,对于不会被用作 base 的 class,不要为其界说 virtual functions,尤其是 virtual deconstructor:Effective C++:Item 7
无法 inline 化
当 virtual function 需求经过 vptr 来调用时,compiler 无法对其进行 inline 优化
class Base
{
public:
virtual bool doSomething() { return true; }
};
class Derived : public Base
{
bool doSomething() override { return false; }
};
Base b;
b.doSomething(); // this can be inlined
Base* b1 = new Derived;
b1->doSomething(); // this cannot
delete b1;
cache miss
运用 virtual function 带来的 cache miss 会远大于运用 template 的状况
example:
static void DynamicPolymorphism() {
std::vector<BaseDP*> ptrs;
for (int i = 0; i < 5000; i++)
{
ptrs.push_back(new DerivedDP);
}
// profiling
for (const auto& ptr : ptrs)
ptr->process();
// Make sure the variable is not optimized away by compiler
// benchmark::DoNotOptimize(created_string);
for (auto& ptr : ptrs)
delete ptr;
}
static void StaticPolymorphism() {
std::vector<BaseSP<DerivedSP>*> ptrs;
for (int i = 0; i < 5000; i++)
{
ptrs.push_back(new BaseSP<DerivedSP>);
}
// profilling
for (const auto& ptr : ptrs)
ptr->process();
for (auto& ptr : ptrs)
delete ptr;
}
运用 cachegrind 和 qcachegrind(mac 上,linux 是 kcachegrind) 查看 cache 命中状况:
其他代替 virtual function 的办法
根据 std::variant
std::variant 是 c++17 引入的类型安全的 union 结构。一个 std::variant 实例只能是其界说的一种类型,很少或许会达到 no value 的状态 (std::variant<Types…>::valueless_by_exception) 。
用 std::variant 完成多态,需求根据一种 visitor 规划模式,即界说好一切的类型,对每个类型编写对应的 function 完成,在实际调用时根据 variant 的 value 类型选择要履行的 function。
example:
struct Type
{
Type(int type) : type_(type) { }
int type_;
};
struct A
{
int f_impl() const { return 1; }
};
struct B
{
int f_impl() const { return 2; }
};
/// 界说 variant
using VAR=std::vector<std::variant<A, B>>;
VAR var;
for (int i = 0; i < 1000; ++i)
{
std::variant<A, B> va;
if (i % 2)
va = A();
else
va = B();
var.push_back(va);
}
/// visit with std::visit
int v_total = 0;
struct CallFunc {
void operator()(const A& a) const { v_total += a.f_impl(); }
void operator()(const B& b) const { v_total += b.f_impl(); }
};
for (int i = 0; i < 100000; ++i)
for (auto & v : var)
std::visit(CallFunc{}, v);
/// visit with holds_alternative
int callFImpl(const std::variant<A, B>& type) {
if (std::holds_alternative<A>(type)) {
return std::get<A>(type).f_impl();
}
return std::get<B>(type).f_impl();
}
total = 0;
for (int i = 0; i < 100000; ++i)
for (auto & v : var)
total += callFImpl(v);
std::variant 与 virtual function 的比较:
功能比较: Quick C++ Benchmarks
根据 concept
首要根据 Replacing CRTP Static Polymorphism With Concepts – Fluent C++ 收拾
CRTP 的问题:
- 多了一层直接的调用语意,可读性比较差,比方下面的代码,需求完成不同的 log 等级输出, 以 CRTP 的写法如下:
template <typename TLoggerImpl>
class Logger {
public:
void LogDebug(std::string_view message) {
Impl().DoLogDebug(message);
}
void LogInfo(std::string_view message) {
Impl().DoLogInfo(message);
}
void LogError(std::string_view message) {
Impl().DoLogError(message);
}
private:
*TLoggerImpl& Impl() { return static_cast<TLoggerImpl&>(*this); }
* friend TLoggerImpl;
};
template <typename TLoggerImpl>
void LogToAll(Logger<TLoggerImpl>& logger, std::string_view message) {
logger.LogDebug(message);
logger.LogInfo(message);
logger.LogError(message);
}
struct CustomLogger : public Logger<CustomLogger> {
void DoLogDebug(std::string_view message) const {
std::cout << "[Debug] " << message << '\n';
}
void DoLogInfo(std::string_view message) const {
std::cout << "[Info] " << message << '\n';
}
void DoLogError(std::string_view message) const {
std::cout << "[Error] " << message << '\n';
}
};
struct TestLogger : public Logger<TestLogger> {
void DoLogDebug(std::string_view) const {}
void DoLogInfo(std::string_view) const {}
void DoLogError(std::string_view) const {}
};
CustomLogger custom_logger;
LogToAll(custom_logger, “Hello World”);
TestLogger test_logger;
LogToAll(test_logger, “Hello World”);
concept 在 c++ 20 引入,用于为 template 的 type 规则限制条件,以下是上面 Log 功能的 concept 完成。
/// 界说一个 concept
template <typename TLoggerImpl>
concept LoggerLike = requires(TLoggerImpl log) {
log.LogDebug(std::string_view{});
log.LogInfo(std::string_view{});
log.LogError(std::string_view{});
};
/// 界说调用类1
struct CustomLogger {
void LogDebug(std::string_view message) const {
std::cout << "[Debug] " << message << '\n';
}
void LogInfo(std::string_view message) const {
std::cout << "[Info] " << message << '\n';
}
void LogError(std::string_view message) const {
std::cout << "[Error] " << message << '\n';
}
};
/// 界说调用类2
struct TestLogger {
void LogDebug(std::string_view) const {}
void LogInfo(std::string_view) const {}
void LogError(std::string_view) const {}
};
/// 界说调用办法
template <LoggerLike TLogger>
void LogToAll(TLogger& logger, std::string_view message) {
logger.LogDebug(message);
logger.LogInfo(message);
logger.LogError(message);
}
struct CustomLoggerImpl { … };
struct TestLoggerImpl { … };
using CustomLogger = Logger<CustomLoggerImpl>;
using TestLogger = Logger<TestLoggerImpl>;
/// 实际调用
CustomLogger custom_logger;
LogToAll(custom_logger, "Hello World");
TestLogger test_logger;
LogToAll(test_logger, "Hello World");
Reference
- Polymorphism in C++ – cppdev
- Performance of dynamic polymorphism – cppdev
- Curiously Recurring Template Pattern – cppreference.com
- Item 7: Declare destructors virtual in polymorphic base classes
- valgrind.org/docs/manual…
- en.cppreference.com/w/cpp/utili…
- Inheritance vs std::variant
- stackoverflow.com/questions/5…
- en.cppreference.com/w/cpp/langu…
- quick-bench.com/q/qKvbnsqH1…
- Concept-based polymorphism in modern C++ GitHub
- Interfaces with C++20 Concepts cppfiddler
- Replacing CRTP Static Polymorphism With Concepts – Fluent C++