美文网首页Clean C++
Clean C++: 为"按值传递"正名

Clean C++: 为"按值传递"正名

作者: 刘光聪 | 来源:发表于2019-06-04 15:08 被阅读0次

C++98中,按值传递(pass-by-value)意味着低效的、不必要的拷贝,被广大程序员嗤之以鼻。按照惯例,对于自定义类型,如果用于入参则应使用pass-by-const-reference;如果用于出参则应pass-by-reference

在缺失移动语义(move semantics)下的C++98标准,这样的结论无可厚非。但是,在增加移动语义(move semantics)下的C++11标准,按值传递在某些特殊场景具有优异的表现力,是时候为"按值传递"正名了。

一个例子

存在两个使用std::function定义的「仿函数」。其中,std::function的泛型参数使用函数的原型表达。例如std::function<bool(int)>的泛型参数是一个一元的谓词。

#include <string>
#include <functional>

using Matcher = std::function<bool(int)>;
using Action = std::function<std::string(int)>;

C++98风格

存在一个Atom的函数对象。按照既有的C++98的习惯,使用pass-by-const-reference传递参数,然后将它们拷贝至私有数据区。此时必然调用std::function的「拷贝构造函数」。

struct Atom {
  Atom(const Matcher& matcher, const Action& action)
    : matcher(matcher), action(action) {
  }
  
  std::string operator()(int m) const {
    return matcher(m) ? action(m) : "";
  }
  
private:
  Matcher matcher;
  Action action;
};

可是,当传递给Atom构造函数的是「右值」时,我们期望调用「移动构造函数」,而非「拷贝构造函数」;因为对于std::function,移动构造相对拷贝构造更加低廉。一般地,针对这个问题,存在3种解决方案,我们逐一分析。

重载函数

使用重载的构造函数,可以准确的根据左值或右值实现函数指派。但是,为了支持两个参数的重载,便要实现4个重载的构造函数,可谓得不偿失。组合爆炸是一种典型的设计缺陷,为了提升程序性能,该方案所付出的成本相当昂贵。

struct Atom {
  Atom(const Matcher& matcher, const Action& action)
    : matcher(matcher), action(action) {
  }
  
  Atom(const Matcher& matcher, Action&& action)
    : matcher(matcher), action(std::move(action)) {
  }
  
  Atom(Matcher&& matcher, const Action& action)
    : matcher(std::move(matcher)), action(action) {
  }

  Atom(Matcher&& matcher, Action&& action)
    : matcher(std::move(matcher)), action(std::move(action)) {
  }
  
  std::string operator()(int m) const {
    return matcher(m) ? action(m) : "";
  }
  
private:
  Matcher matcher;
  Action action;
};

成本分析

当传递左值,存在一次「拷贝构造」;当传递右值,存在一次「移动构造」。但是,存在不可接受的组合爆炸的问题。

透传引用

「透传引用(Forward Reference)」是C++11实现「完美转换(Perfect Forward)」机制而引入的一个概念。使用「透传引用」可以合并上述4个构造函数,实现左值和右值的透明传递和分发。但是,使用「透传引用」引入了泛型设计,其实现必须放到头文件,增加了编译时依赖的成本,极大地增加了实现的复杂度。而且,因为模板而导致代码膨胀的问题,相对上述重载方案其目标代码规模并没有得到改善,甚至更糟。此外,鉴于「完美转换」并非100%的完美,当客户传递不正确的类型时,编译器的错误信息相当冗长。

struct Atom {
  template <typename Matcher, typename Action>
  Atom(Matcher&& matcher, Action&& action)
    : matcher(std::forward<Matcher>(matcher))
    , action(std::forward<Action>(action)) {
  }
  
  std::string operator()(int m) const {
    return matcher(m) ? action(m) : "";
  }
  
private:
  Matcher matcher;
  Action action;
};

成本分析

当传递左值,存在一次「拷贝构造」;当传递右值,存在一次「移动构造」。避免了组合爆炸的问题,但引入了模板的复杂度,及其代码膨胀的问题。但是,相对重载方法,代码实现更加简洁,更加紧凑。

按值传递

考虑使用pass-by-value的方式传递MatcherAction,不仅实现简单,天然支持左值或右值,而且成本在接受的范围之内。

struct Atom {
  Atom(Matcher matcher, Action action)
    : matcher(std::move(matcher))
    , action(std::move(action)) {
  }
  
  std::string operator()(int m) const {
    return matcher(m) ? action(m) : "";
  }
  
private:
  Matcher matcher;
  Action action;
};

成本分析

当传递左值,存在一次「拷贝构造」,及其一次「移动构造」;当传递右值,存在两次次「移动构造」。相对上两个方案,两种场景其成本均多了一次低廉的「移动构造」。但是,该方案完全避免了组合爆炸的问题,及其模板的复杂度,代码最为简洁。

何时按值传递

综上述,可以归纳如下条件,可以考虑使用「按值传递」的方法。

  • 产生副本;
  • 类型可拷贝;
  • 移动成本低廉。

例如,上述的函数对象Atom,完全满足上述必要条件。

  • 产生副本:通过「拷贝构造」或「移动构造」,产生私有的matcheraction副本;
  • 类型可拷贝:std::function类型可拷贝。
  • 移动成本低廉:std::function的移动构造的成本非常低廉。

在Lambda中的应用

事实上,C++98风格的函数对象,都可以使用Lambda简化实现。例如,原来的函数对象Atom实现,可以使用更为简洁的Lambda表达式实现。

using Rule = std::function<std::string(int)>;

Rule atom(Matcher matcher, Action action) {
  return [matcher, action](int m) {
    return matcher(m) : action(m) : "";
  };
}

但是,在Lambda表达式的「参数捕获列表」中,matcher, action是按值传递给闭包对象的,此时调用的是std::function的「拷贝构造函数」。幸运的是,C++14支持以初始化的方式将对象移动至闭包之中,弥补了这个遗憾。

using Rule = std::function<std::string(int)>;

Rule atom(Matcher matcher, Action action) {
  return [matcher = std::move(matcher), action = std::move(action)](int m) {
    return matcher(m) : action(m) : "";
  };
}

相关文章

  • Clean C++: 为"按值传递"正名

    在C++98中,按值传递(pass-by-value)意味着低效的、不必要的拷贝,被广大程序员嗤之以鼻。按照惯例,...

  • 读书笔记17.06.02【stack】【vector】

    C++中参数传递:按值传递,指针传递和引用传递按值传递:形参是实参的拷贝。指针传递:拷贝指针,被调用函数对指针指向...

  • 再学JS--函数参数传递类型

    JavaScript的函数参数传递分为按值传递、按引用传递以及按共享传递。 按值传递 什么是按值传递? 把函数外部...

  • 按值传递、按引用传递、按共享传递

    按值传递、按引用传递、按共享传递 按值传递(call by value) 按值传递,就是指在调用函数时,将实参对应...

  • JS是按值传递还是按引用传递?

    JS是按值传递还是按引用传递? 按值传递 VS. 按引用传递 探究JS值的传递方式 按共享传递 call by s...

  • 解读Java参数传递

    Java语言的传递方式只有“按值传递”!“按值传递”! “按值传递”!重要的事情要说三遍。不过呢,按值传递可能还不...

  • C++ 引用传递的学习

    C++ 引用与引用作为函数的参数C++函数的三种传递方式为:值传递、指针传递和引用传递 C++ 上课习题 刘月林2...

  • Java中的参数传递

    为了便于理解,会将参数传递分为按值传递和按引用传递。按值传递是传递的值的拷贝,按引用传递传递的是引用的地址值,所以...

  • Java按值传递与按引用传递(区别)

    在C++中我们进行参数传递的时候,往往会遇到按值传递与按引用传递的情况,但是在Java中并没有引用这个概念,在今天...

  • 聊聊Java内部类

    一.磨叽磨叽Java值传递与引用传递 “在Java里面参数传递都是按值传递”即:按值传递是传递的值的拷贝,按引用传...

网友评论

    本文标题:Clean C++: 为"按值传递"正名

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