美文网首页C++ Templates
【C++ Templates(20)】桥接静态多态与动态多态

【C++ Templates(20)】桥接静态多态与动态多态

作者: downdemo | 来源:发表于2018-06-22 15:45 被阅读19次

函数对象、指针与std::function<>

  • 函数对象用于给模板提供一些可定制行为,比如下面的函数模板枚举从0到某个值的整型值,每个值被提供给函数对象f
// bridge/forupto1.cpp

#include <vector>
#include <iostream>

template<typename F>
void forUpTo(int n, F f)
{
    for (int i = 0; i != n; ++i)
    {
        f(i); // call passed function f for i
    }
}

void printInt(int i)
{
    std::cout << i << ' ';
}

int main()
{
    std::vector<int> values;
    // insert values from 0 to 4:
    forUpTo(5,
        [&values](int i) {
            values.push_back(i);
        });

    // print elements:
    forUpTo(5, printInt); // prints 0 1 2 3 4
    std::cout << '\n';
}
  • forUpTo()函数模板能用于任何函数对象,包括lambda、函数指针、任何实现合适的operator()或到函数指针、引用的转换的类,每次使用forUpTo()都将产生一个不同的实例化。这个例子的模板很小,如果十分大则这些实例化可能大大增加代码大小。一个限制代码增长的方法是把函数模板改为不需要实例化的非模板,如使用函数指针
// bridge/forupto2.hpp
void forUpTo(int n, void (*f)(int))
{
    for (int i = 0; i != n; ++i)
    {
        f(i); // call passed function f for i
    }
}
  • 但这个实现在传递lambda时会出错
forUpTo(5, printInt); // OK: prints 0 1 2 3 4
forUpTo(5,
    [&values](int i) { // ERROR: lambda not convertible to a function pointer
        values.push_back(i);
    });
  • 标准库类模板std::function<>提供了一个forUpTo()可选用的构建
// bridge/forupto3.hpp

#include <functional>
void forUpTo(int n, std::function<void(int)> f)
{
    for (int i = 0; i != n; ++i)
    {
        f(i); // call passed function f for i
    }
}
  • std::function<>的模板实参是一个函数类型,它描述函数对象将接收的参数类型和产生的返回类型。这个forUpTo()的构建提供了一些静态多态方面的特点——能够处理无界集合的类型,包括函数指针、lambda、任意带合适的operator()的类。它使用类型擦除的技术做到这点,类型擦除桥接了静态多态与动态多态的间隔

广义的函数指针

  • std::function<>类型是一个广义的函数指针形式,它提供了同样的基本操作
    • 可用于调用函数而不需要调用者知道关于函数本身的任何东西
    • 可以被拷贝、移动和赋值
    • 可由另一个函数初始化或赋值
    • 有一个null状态来表明没有绑定函数
  • 然而不同于函数指针的是,std::function<>也能存储一个lambda或其他任何带有合适的operator()的函数对象
  • 接下来建立一个广义函数指针类模板FunctionPtr,它同样会提供这些核心操作并能用于代替std::function
// bridge/forupto4.cpp

#include "functionptr.hpp"
#include <vector>
#include <iostream>

void forUpTo(int n, FunctionPtr<void(int)> f)
{
    for (int i = 0; i != n; ++i)
    {
        f(i); // call passed function f for i
    }
}

void printInt(int i)
{
    std::cout << i << ' ';
}

int main()
{
    std::vector<int> values;

    // insert values from 0 to 4:
    forUpTo(5,
        [&values](int i) {
            values.push_back(i);
        });

    // print elements:
    forUpTo(5, printInt); // prints 0 1 2 3 4
    std::cout << '\n';
}
  • FunctionPtr的接口是很明显的,需要提供构造、拷贝、移动、析构、初始化、赋值、调用等操作。最有趣的部分是如何在一个类模板局部特化中描述接口,以用于将模板实参(一个函数类型)分解为其组成部分(结果和实参类型)
// bridge/functionptr.hpp

// primary template:
template<typename Signature>
class FunctionPtr;

// partial specialization:
template<typename R, typename... Args>
class FunctionPtr<R(Args...)>
{
private:
    FunctorBridge<R, Args...>* bridge;
public:
    // constructors:
    FunctionPtr() : bridge(nullptr) {
    }
    FunctionPtr(FunctionPtr const& other); // see functionptrcpinv.hpp
    FunctionPtr(FunctionPtr& other)
    : FunctionPtr(static_cast<FunctionPtr const&>(other)) {
    }
    FunctionPtr(FunctionPtr&& other) : bridge(other.bridge) {
        other.bridge = nullptr;
    }

    //construction from arbitrary function objects:
    template<typename F> FunctionPtr(F&& f); // see functionptrinit.hpp

    // assignment operators:
    FunctionPtr& operator=(FunctionPtr const& other) {
        FunctionPtr tmp(other);
        swap(*this, tmp);
        return *this;
    }
    FunctionPtr& operator=(FunctionPtr&& other) {
        delete bridge;
        bridge = other.bridge;
        other.bridge = nullptr;
        return *this;
    }

    //construction and assignment from arbitrary function objects:
    template<typename F> FunctionPtr& operator=(F&& f) {
        FunctionPtr tmp(std::forward<F>(f));
        swap(*this, tmp);
        return *this;
    }

    // destructor:
    ~FunctionPtr() {
        delete bridge;
    }

    friend void swap(FunctionPtr& fp1, FunctionPtr& fp2) {
        std::swap(fp1.bridge, fp2.bridge);
    }
    explicit operator bool() const {
        return bridge == nullptr;
    }

    // invocation:
    R operator()(Args... args) const; // see functionptr-cpinv.hpp
};
  • 这个实现包含一个单独的非静态成员变量bridge,它将负责存储的函数对象的存储的操作。这个指针的所有权绑定到FunctionPtr对象上,因此提供的大多实现只是管理这个指针

桥接口

  • FunctorBridge类模板负责函数对象的所有权和操作,它实现为一个抽象基类,构成FunctionPtr动态多态的基础
// bridge/functorbridge.hpp

template<typename R, typename... Args>
class FunctorBridge
{
public:
    virtual ~FunctorBridge() {
    }
    virtual FunctorBridge* clone() const = 0;
    virtual R invoke(Args... args) const = 0;
};
  • FunctorBridge通过虚函数提供必要的操作:析构函数、clone()、invoke()
// bridge/functionptr-cpinv.hpp

template<typename R, typename... Args>
FunctionPtr<R(Args...)>::FunctionPtr(FunctionPtr const& other)
: bridge(nullptr)
{
    if (other.bridge) {
        bridge = other.bridge->clone();
    }
}

template<typename R, typename... Args>
R FunctionPtr<R(Args...)>::operator()(Args... args) const
{
    return bridge->invoke(std::forward<Args>(args)...);
}

类型擦除(Type Erasure)

  • FunctorBridge实例是一个抽象类,因此派生类负责提供虚函数的具体实现。为了支持整个范围的潜在函数对象——一个无界集合——需要无界数量的派生类。要实现这点,可基于派生类存储的函数对象类型,参数化派生类
// bridge/specificfunctorbridge.hpp

template<typename Functor, typename R, typename... Args>
class SpecificFunctorBridge : public FunctorBridge<R, Args...> {
    Functor functor;
public:
    template<typename FunctorFwd>
    SpecificFunctorBridge(FunctorFwd&& functor)
    : functor(std::forward<FunctorFwd>(functor)) {
    }
    virtual SpecificFunctorBridge* clone() const override {
        return new SpecificFunctorBridge(functor);
    }
    virtual R invoke(Args... args) const override {
        return functor(std::forward<Args>(args)...);
    }
};
  • SpecificFunctorBridge的实例化存储一个函数对象的拷贝,它能被调用、拷贝或析构。无论何时一个FunctionPtr初始化为一个新函数对象,都将创建一个SpecificFunctorBridge实例
// bridge/functionptr-init.hpp
template<typename R, typename... Args>
template<typename F>
FunctionPtr<R(Args...)>::FunctionPtr(F&& f)
: bridge(nullptr)
{
    using Functor = std::decay_t<F>;
    using Bridge = SpecificFunctorBridge<Functor, R, Args...>;
    bridge = new Bridge(std::forward<F>(f));
}
  • 注意FunctionPtr构造函数本身基于函数对象类型F模板化,那个类型只被SpecificFunctorBridge的局部特化知道。一旦新分配的Bridge实例赋值给数据成员bridge,关于特定类型F的额外信息就会因派生类到基类的转换(Bridge*到 FunctorBridge<R, Args...> *)而丢失。这个类型信息的丢失解释了为何术语“类型擦除”常用于描述桥接静态与动态多态的技术

可选桥接

  • FunctionPtr还不支持函数指针提供的一个操作:测试是否两个FunctionPtr对象将调用相同的函数。添加这样一个操作需要FunctorBridge更新一个equals操作
virtual bool equals(FunctorBridge const* fb) const = 0;
  • 接着在SpecificFunctorBridge中实现
virtual bool equals(FunctorBridge<R, Args...> const* fb) const override
{
    if (auto specFb = dynamic_cast<SpecificFunctorBridge const*> (fb))
    {
        return functor == specFb->functor;
    }
    // functors with different types are never equal:
    return false;
}
  • 最终为FunctionPtr实现operator==
friend bool
operator==(FunctionPtr const& f1, FunctionPtr const& f2) {
    if (!f1 || !f2) {
        return !f1 && !f2;
    }
    return f1.bridge->equals(f2.bridge);
}

friend bool
operator!=(FunctionPtr const& f1, FunctionPtr const& f2) {
    return !(f1 == f2);
}
  • 这个实现是正确的,但有一个缺点:如果FunctionPtr用没有operator==的函数对象赋值或初始化,编译将报错。这可能带来意外,因为FunctionPtr的operator==还没被使用,而许多其他类模板,如std::vector可以用没有operator==的类型实例化,只要operator==没被使用
  • 这个问题来源于类型擦除:因为一旦FunctionPtr被赋值或初始化,实际上就丢失了函数对象的类型,这就需要在赋值或实例化完成前捕获所有需要知道的类型信息。这个信息包括构建一个函数对象的operator==的调用,因为不能确定何时需要它
  • 可以用SFINAE-based traits在调用前查询operator==是否可用
// bridge/isequalitycomparable.hpp

#include <utility> // for declval()
#include <type_traits> // for true_type and false_type

template<typename T>
class IsEqualityComparable
{
private:
    // test convertibility of == and ! == to bool:
    static void* conv(bool); // to check convertibility to bool
    template<typename U>
    static std::true_type test(decltype(conv(std::declval<U const&>() ==
            std::declval<U const&>())),
        decltype(conv(!(std::declval<U const&>() ==
            std::declval<U const&>())))
    );

    // fallback:
    template<typename U>
    static std::false_type test(...);
public:
    static constexpr bool value = decltype(test<T>(nullptr, nullptr))::value;
};
  • IsEqualityComparable用了典型的表达式测试的构成:两个test()重载,一个包含包裹在decltype中需要测试的表达式,另一个接收任意数量实参
  • 使用IsEqualityComparable可以构造一个TryEquals类模板,可以调用给定类型的==或在没有合适的==时抛出异常
// bridge/tryequals.hpp

#include <exception>
#include "isequalitycomparable.hpp"

template<typename T,
    bool EqComparable = IsEqualityComparable<T>::value>
struct TryEquals
{
    static bool equals(T const& x1, T const& x2) {
        return x1 == x2;
    }
};

class NotEqualityComparable : public std::exception
{
};

template<typename T>
struct TryEquals<T, false>
{
    static bool equals(T const& x1, T const& x2) {
        throw NotEqualityComparable();
    }
};
  • 最终,通过使用TryEquals就能在FunctionPtr中提供对==的支持
virtual bool equals(FunctorBridge<R, Args...> const* fb) const override
{
    if (auto specFb = dynamic_cast<SpecificFunctorBridge const*> (fb))
    {
        return TryEquals<Functor>::equals(functor, specFb->functor);
    }
    // functors with different types are never equal:
    return false;
}

性能考虑

  • 类型擦除同时提供了一些但非全部的静态多态和动态多态的优点。特别地,使用类型擦除的泛型代码的性能更接近于动态多态,因为两者都通过虚函数使用动态调度。因此,一些传统的静态多态的优点,如编译器内联调用的能力,可能丢失。这种性能损失是否可感知是依赖于应用程序的,但通常很容易判断,考虑相对虚函数调用开销需要执行多少工作:如果两者接近,如使用FunctionPtr相加两个整数,类型擦除可能执行得比静态多态版本慢得多,反之如果函数调用执行大量工作,如查询数据库、排序容器或更新用户接口,类型擦除的开销不太可能是可测量的

相关文章

  • 【C++ Templates(20)】桥接静态多态与动态多态

    函数对象、指针与std::function<> 函数对象用于给模板提供一些可定制行为,比如下面的函数模板枚举从0到...

  • 19 桥接静态多态与动态多态

    函数对象[https://en.cppreference.com/w/cpp/named_req/Function...

  • 查漏补缺

    C++虚函数: 多态: 静态多态(重载)、动态多态(虚函数) 虚函数 虚函数表:编译器为每个类创建了一个虚函数表...

  • 动态多态、静态多态

    C++支持多种形式的多态,从表现的形式来看,有虚函数、模板、重载等,从绑定时间来看,可以分成静态多态和动态多态,也...

  • 面试题目收集总结

    C++: 多态: 多态性都有哪些?(静态和动态,然后分别叙述了一下虚函数和函数重载) c语言和c++有什么区别?(...

  • 多态(1)静态多态与动态多态

    多态(1)静态多态与动态多态 什么是多态 从字面上理解就是多种形态的意思。而多态一词最初源自希腊语,其含义便是“多...

  • 深刻剖析之c++博客文章

    三大特性 封装、继承、多态 多态 C++ 虚函数表解析C++多态的实现原理 介绍了类的多态(虚函数和动态/迟绑定)...

  • 二继承与多态——第四节、多态

    文章目录1、 从吃烤山药重新认识多态2、 多态前提条件【重点】3、 多态的体现4、 多态动态绑定与静态绑定4、1....

  • 多态和虚函数

    多态可以分为静态多态和动态多态 静态多态:函数重载,泛型编程,编译器在编译期间内完成的,编译器根据函数实参的类型可...

  • c++ ——为什么要使用多态?

    多态可分为静态多态和动态多态 静态多态就是在系统编译期间就可以确定程序执行到这里将要执行哪个函数,比如函数的重载。...

网友评论

    本文标题:【C++ Templates(20)】桥接静态多态与动态多态

    本文链接:https://www.haomeiwen.com/subject/eabkyftx.html