Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

c++11-17 模板核心知识(十一)—— 编写泛型库需要的基本技术 #161

Open
zhangyachen opened this issue Dec 2, 2020 · 0 comments
Labels

Comments

@zhangyachen
Copy link
Owner

Callables

许多基础库都要求调用方传递一个可调用的实体(entity)。例如:一个描述如何排序的函数、一个如何hash的函数。一般用callback来描述这种用法。在C++中有以下几种形式可以实现callback,它们都可以被当做函数参数传递并可以直接使用类似f(...)的方式调用:

  • 指向函数的指针。
  • 重载了operator()的类(有时被叫做functors),包括lambdas.
  • 包含一个可以生成函数指针或者函数引用的转换函数的类。

C++使用callable type来描述上面这些类型。比如,一个可以被调用的对象称作callable object,我们使用callback来简化这个称呼。

编写泛型代码会因为这个用法的存在而可扩展很多。

函数对象 Function Objects

例如一个for_each的实现:

template <typename Iter, typename Callable>
void foreach (Iter current, Iter end, Callable op) {
  while (current != end) {     // as long as not reached the end
    op(*current);              // call passed operator for current element
    ++current;                 // and move iterator to next element
  }
}

使用不同的Function Objects来调用这个模板:

// a function to call:
void func(int i) { std::cout << "func() called for: " << i << '\n'; }

// a function object type (for objects that can be used as functions):
class FuncObj {
public:
  void operator()(int i) const { // Note: const member function
    std::cout << "FuncObj::op() called for: " << i << '\n';
  }
};


int main(int argc, const char **argv) {
  std::vector<int> primes = {2, 3, 5, 7, 11, 13, 17, 19};

  foreach (primes.begin(), primes.end(),  func);       // range function as callable (decays to pointer)
  foreach (primes.begin(), primes.end(), &func);         // range function pointer as callable

  foreach (primes.begin(), primes.end(), FuncObj());     // range function object as callable
                                              
  foreach (primes.begin(), primes.end(),     // range lambda as callable
           [](int i) {                   
             std::cout << "lambda called for: " << i << '\n';
           });
  return 0;
}

解释一下:

  • foreach (primes.begin(), primes.end(), func); 按照值传递时,传递函数会decay为一个函数指针。
  • foreach (primes.begin(), primes.end(), &func); 这个比较直接,直接传递了一个函数指针。
  • foreach (primes.begin(), primes.end(), FuncObj()); 这个是上面说过的functor,一个重载了operator()的类。所以,当调用op(*current);时,实际是在调用op.operator()(*current);. ps. 如果不加函数声明后面的const,在某些编译器中可能会报错。
  • Lambda : 这个和前面情况一样,不解释了。

处理成员函数及额外的参数

上面没有提到一个场景 : 成员函数。因为调用非静态成员函数的方式是object.memfunc(. . . )ptr->memfunc(. . . ),不是统一的function-object(. . . )

std::invoke<>()

幸运的是,从C++17起,C++提供了std::invoke<>()来统一所有的callback形式:

image

template <typename Iter, typename Callable, typename... Args>
void foreach (Iter current, Iter end, Callable op, Args const &... args) {
  while (current != end) {     // as long as not reached the end of the elements
    std::invoke(op,            // call passed callable with
                args...,       // any additional args
                *current);     // and the current element
    ++current;
  }
}

那么,std::invoke<>()是怎么统一所有callback形式的呢?
注意,我们在foreach中添加了第三个参数:Args const &... args. invoke是这么处理的:

  • **如果Callable是指向成员函数的指针,**它会使用args的第一个参数作为类的this。args中剩余的参数被传递给Callable。
  • 否则,所有args被传递给Callable。

使用:

// a class with a member function that shall be called
class MyClass {
public:
  void memfunc(int i) const {
    std::cout << "MyClass::memfunc() called for: " << i << '\n';
  }
};

int main() {
  std::vector<int> primes = {2, 3, 5, 7, 11, 13, 17, 19};

  // pass lambda as callable and an additional argument:
  foreach (
      primes.begin(), primes.end(),              // elements for 2nd arg of lambda
      [](std::string const &prefix, int i) {     // lambda to call
        std::cout << prefix << i << '\n';
      },
      "- value: ");    // 1st arg of lambda

  // call obj.memfunc() for/with each elements in primes passed as argument
  MyClass obj;
  foreach (primes.begin(), primes.end(), // elements used as args
           &MyClass::memfunc,            // member function to call
           obj);                         // object to call memfunc() for
}

注意在callback是成员函数的情况下,是如何调用foreach的。

统一包装

std::invoke()的一个场景用法是:包装一个函数调用,这个函数可以用来记录函数调用日志、测量时间等。

#include <utility>               // for std::invoke()
#include <functional>        // for std::forward()

template<typename Callable, typename... Args>
decltype(auto) call(Callable&& op, Args&&... args) {
    return std::invoke(std::forward<Callable>(op),  std::forward<Args>(args)...);       // passed callable with any additional args
}

一个需要考虑的事情是,如何处理op的返回值并返回给调用者:

template<typename Callable, typename... Args>
decltype(auto) call(Callable&& op, Args&&... args)

这里使用decltype(auto)(从C++14起)(decltype(auto)的用法可以看之前的文章 : c++11-17 模板核心知识(九)—— 理解decltype与decltype(auto))

如果想对返回值做处理,可以声明返回值为decltype(auto)

decltype(auto) ret{std::invoke(std::forward<Callable>(op), std::forward<Args>(args)...)};

...
return ret;

但是有个问题,使用decltype(auto)声明变量,值不允许为void,可以针对void和非void分别进行处理:

#include <functional>  // for std::forward()
#include <type_traits> // for std::is_same<> and invoke_result<>
#include <utility>     // for std::invoke()

template <typename Callable, typename... Args>
decltype(auto) call(Callable &&op, Args &&... args) {

  if constexpr (std::is_same_v<std::invoke_result_t<Callable, Args...>, void>) {
    // return type is void:
    std::invoke(std::forward<Callable>(op), std::forward<Args>(args)...);
    ... 
    return;
  } else {
    // return type is not void:
    decltype(auto) ret{
        std::invoke(std::forward<Callable>(op), std::forward<Args>(args)...)};
    ... 
    return ret;
  }
}

std::invoke_result<>只有从C++17起才能使用,C++17之前只能用typename std::result_of<Callable(Args...)>::type.

泛型库的其他基本技术

Type Traits

这个技术很多人应该很熟悉,这里不细说了。

#include <type_traits>

template <typename T> 
class C {

  // ensure that T is not void (ignoring const or volatile):
  static_assert(!std::is_same_v<std::remove_cv_t<T>, void>,
                "invalid instantiation of class C for void type");

public:
  template <typename V> void f(V &&v) {
    if constexpr (std::is_reference_v<T>) {
      ... // special code if T is a reference type
    }
    if constexpr (std::is_convertible_v<std::decay_t<V>, T>) {
      ... // special code if V is convertible to T
    }
    if constexpr (std::has_virtual_destructor_v<V>) {
      ... // special code if V has virtual destructor
    }
  }
};

这里,我们使用type_traits来进行不同的实现。

std::addressof()

可以使用std::addressof<>()获取对象或者函数真实的地址, 即使它重载了operator &. 不过这种情况不是很常见。当你想获取任意类型的真实地址时,推荐使用std::addressof<>():

template<typename T>
void f (T&& x) {
    auto p = &x;         // might fail with overloaded operator &
    auto q = std::addressof(x);       // works even with overloaded operator &
    ...
}

比如在STL vector中,当vector需要扩容时,迁移新旧vector元素的代码:

{
  for (; __first != __last; ++__first, (void)++__cur) std::_Construct(std::__addressof(*__cur), *__first);
  return __cur;
}

template <typename _T1, typename... _Args>
inline void _Construct(_T1 *__p, _Args &&... __args) {
  ::new (static_cast<void *>(__p)) _T1(std::forward<_Args>(__args)...);      //实际copy(或者move)元素
}

这里使用std::addressof()获取新vector当前元素的地址,然后进行copy(或move)。可以看之前写的c++ 从vector扩容看noexcept应用场景

std::declval

std::declval可以被视为某一特定类型对象引用的占位符。它不会创建对象,常常和decltype和sizeof搭配使用。因此,在不创建对象的情况下,可以假设有相应类型的可用对象,即使该类型没有默认构造函数或该类型不可以创建对象。

注意,declval只能在unevaluated contexts中使用。

一个简单的例子:

class Foo;     //forward declaration
Foo f(int);     //ok. Foo is still incomplete
using f_result = decltype(f(11));      //f_result is Foo

现在如果我想获取使用int调用f()后返回的类型是什么?是decltype(f(11))?看起来怪怪的,使用declval看起来就很明了:

decltype(f(std::declval<int>()))

还有就是之前c++11-17 模板核心知识(一)—— 函数模板中的例子)——返回多个模板参数的公共类型:

template <typename T1, typename T2,
          typename RT = std::decay_t<decltype(true ? std::declval<T1>()
                                                   : std::declval<T2>())>>
RT max(T1 a, T2 b) {
  return b < a ? a : b;
}

这里在为了避免在?:中不得不去调用T1 和T2 的构造函数去创建对象,我们使用declval来避免创建对象,而且还可以达到目的。ps. 别忘了使用std::decay_t,因为declval返回的是一个rvalue references. 如果不用的话,max(1,2)会返回int&&.

最后看下官网的例子:

#include <utility>
#include <iostream>
 
struct Default { int foo() const { return 1; } };
 
struct NonDefault
{
    NonDefault() = delete;
    int foo() const { return 1; }
};
 
int main()
{
    decltype(Default().foo()) n1 = 1;                   // type of n1 is int
//  decltype(NonDefault().foo()) n2 = n1;               // error: no default constructor
    decltype(std::declval<NonDefault>().foo()) n2 = n1;    // type of n2 is int
    std::cout << "n1 = " << n1 << '\n'
              << "n2 = " << n2 << '\n';
}

完美转发 Perfect Forwarding

template<typename T>
void f (T&& t) // t is forwarding reference {
    g(std::forward<T>(t));       // perfectly forward passed argument t to g()
}

或者转发临时变量,避免无关的拷贝开销:

template<typename T>
void foo(T x) {
    auto&& val = get(x);
    ...

    // perfectly forward the return value of get() to set():
    set(std::forward<decltype(val)>(val));
}

作为模板参数的引用

template<typename T>
void tmplParamIsReference(T) {
    std::cout << "T is reference: " << std::is_reference_v<T> << '\n';
}

int main() {
    std::cout << std::boolalpha;
    int i;
    int& r = i;
    tmplParamIsReference(i);     // false
    tmplParamIsReference(r);      // false
    tmplParamIsReference<int&>(i);      // true
    tmplParamIsReference<int&>(r);      // true
}

这点也不太常见,在前面的文章c++11-17 模板核心知识(七)—— 模板参数 按值传递 vs 按引用传递提到过一次。这个会改变强制改变模板的行为,即使模板的设计者一开始不想这么设计。

我没怎么见过这种用法,而且这种用法有的时候会有坑,大家了解一下就行。

可以使用static_assert禁止这种用法:

template<typename T>
class optional {
    static_assert(!std::is_reference<T>::value, "Invalid instantiation of optional<T> for references");
    …
};

延迟计算 Defer Evaluations

首先引入一个概念:incomplete types. 类型可以是complete或者incomplete,incomplete types包含:

  • 类只声明没有定义。
  • 数组没有定义大小。
  • 数组包含incomplete types。
  • void
  • 枚举类型的underlying type或者枚举类型的值没有定义。

可以理解incomplete types为只是定义了一个标识符但是没有定义大小。例如:

class C;     // C is an incomplete type
C const* cp;     // cp is a pointer to an incomplete type
extern C elems[10];     // elems has an incomplete type
extern int arr[];     // arr has an incomplete type
...
class C { };     // C now is a complete type (and therefore cpand elems no longer refer to an incomplete type)
int arr[10];     // arr now has a complete type

现在回到Defer Evaluations的主题上。考虑如下类模板:

template<typename T>
class Cont {
  private:
    T* elems;
  public:
    ...
};

现在这个类可以使用incomplete type,这在某些场景下很重要,例如链表节点的简单实现:

struct Node {
    std::string value;
    Cont<Node> next;        // only possible if Cont accepts incomplete types
};

但是,一旦使用一些type_traits,类就不再接受incomplete type:

template <typename T> 
class Cont {
private:
  T *elems;

public:
  ... 
  
  typename std::conditional<std::is_move_constructible<T>::value, T &&, T &>::type 
  foo();
};

std::conditional也是一个type_traits,这里的意思是:根据T是否支持移动语义,来决定foo()返回T &&还是T &.

但是问题在于,std::is_move_constructible需要它的参数是一个complete type. 所以,之前的struct Node这种声明会失败(不是所有的编译器都会失败。其实这里我理解不应该报错,因为按照类模板实例化的规则,成员函数只有用到的时候才进行实例化)。

我们可以使用Defer Evaluations来解决这个问题:

template <typename T> 
class Cont {
private:
  T *elems;

public:
  ... 
  
  template<typename D = T>
  typename std::conditional<std::is_move_constructible<T>::value, T &&, T &>::type 
  foo();
};

这样,编译器就会直到foo()被complete type的Node调用时才实例化。

(完)

朋友们可以关注下我的公众号,获得最及时的更新:

@zhangyachen zhangyachen added the cpp label Dec 2, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

1 participant