关于c++11:理解-stddeclval-和-decltype

7次阅读

共计 5422 个字符,预计需要花费 14 分钟才能阅读完成。

std::declvaldecltype

题图来自于 C++ Type Deduction Introduction – hacking C++ 但略有变形以适宜 banner

对于 decltype

decltype(expr) 是一个 C++11 新增的关键字,它的作用是将实体或者表达式的类型求进去。

#include <iostream>
int main() {
  int i = 33;
  decltype(i) j = i * 2;
  std::cout << j;
}

它很简略,无需额定解释。

但如此简略的一个货色,怎么就须要新增一个关键字这么大件事呢?还是元编程闹的!元编程世界里,长的狐疑人生的一串模板类申明让人解体,反复书写它们更是累赘。例如一条运行时调试日志输入:

这不是我印象中最长的名称,只是最棘手就能截取的一个援用。这样的例子多的是。

借用我的 谈 C++17 里的 State 模式之二 也即 fsm-cxx 的应用例子稍加改写来体现 decltype 的用途:

void test_state_meta() {
  machine_t<my_state, void, payload_t<my_state>> m;
  using M = decltype(m);
  // equals to: using M = machine_t<my_state, void, payload_t<my_state>>;

  // @formatter:off
  // states
  m.state().set(my_state::Initial).as_initial().build();
  // ...
}

显然,using M = decltype(m) 更简练,特地是当 machine_t<my_state, void, payload_t<my_state>> 可能是一超级长带超级多模板参数的定义的字串时,decltype 的价值还会体现的更显著。

在元编程里,特地是波及到大型类体系彼此纠结的情景时,很多时候可能不能不借助 decltype 的能力以及 auto 主动推导能力,因为在一个具体场景中可能咱们不能预设具体类型会是什么。

规范化的编码格调

此外,善用 decltype 和 using 可能为你的代码规范性和编码省力性上贡献力量。

在编写一个类时,咱们应该多加应用 using 提供的类型别名能力,当然同时这其中可能会波及到对 decltype 的使用。

应用 using 的益处在于,能够提前显式地督促编译器进行相干类型推导,如果有错能够在一组 using 语句处进行修改,不用在一大堆代码段落中去钻研为何类型用错。

用错了类型又可能引发大堆代码的被迫改写。

应用 using 还能帮忙你缩小代码段落批改。例如 using Container=std::list<T> 改为 using Container=std::vector<T> 时,你的曾经写就的代码段落乃至于 Container _container 申明均能够一丝一毫不做批改,只须要从新编译就够。

本大节不给参考用例,因为那会喧宾夺主。而且机会不到,讲给你听也不起作用。

对于 std::declval

std::declval<T>() 也没什么好说的,它能返回类型 T 的右值援用参考。

然而 cppref 讲的真是云里雾里,说到底 declval 到底能干什么?它就是用于返回一个 T 对象的伪造实例,同时又具备右值援用参考。换句话说,它等价于上面的 objref 的编译期态:

T obj{};
T &objref = obj{};

首先,它在词法和语义上等价于 objref,是对象 T 的实例值,且具备 T&& 的类型;其次,它仅用于非求值的场合;再次,它并不真的存在。啥意思,说人话就是在编译期中,须要一个值对象,但并不心愿这个值对象被编译为一个二进制实体,那就用 declval 虚构地结构一个,从而彷佛取得了一个长期对象,能够在该对象上施加操作,例如调用成员函数什么的,但既然是虚构的,就不会真的存在这么个长期对象,所以我称之为伪实例。

咱们经常并不真的间接须要 declval 求值求得的伪实例,更多的是须要借助于这个伪实例来求取到相应的类型形容,也就是 T。所以个别状况下 declval 之外往往突围着 decltype 计算,设法拿到 T 才是咱们的实在目标:

#include <iostream>

namespace {struct base_t { virtual ~base_t(){}};

  template<class T>
    struct Base : public base_t {virtual T t() = 0;
    };

  template<class T>
    struct A : public Base<T> {~A(){}
      virtual T t() override { std::cout << "A" << '\n'; return T{}; }
    };
}

int main() {decltype(std::declval<A<int>>().t()) a{}; // = int a;
  decltype(std::declval<Base<int>>().t()) b{}; // = int b;
  std::cout << a << ',' << b << '\n';
}

能够看到,A<int> 的伪实例可能“调用”A 的成员函数 t(),而后借助于 decltype 咱们就能够拿到 t() 的返回类型,并用来申明一个具体的变量 a。因为 t() 的返回类型为 T,所以 main() 函数中的这条变量申明语句实际上等价于 int a{};

这个例子是为了帮忙你了解 declval 的理论含意,例子自身是比拟无意义的。

declval 的力量

declval(expr) 的外围力量在下面的例子中显示的很明确:它不会对 expr 真正求值。所以你不用在 expr 处产生任何长期对象,也不会因为表达式很简单而产生实在的计算。这对于元编程的简单环境是十分有用的。

上面的来自于某 ppt 的一页还展现了表达式不用求值仅求类型的用例:

FROM: HERE

但不仅如此,declval 的不求值还衍生出了进一步的力量。

无默认构造函数

如果一个类没有定义默认构造函数,在元编程环境中可能是很麻烦的。例如上面的 decltype 就无奈通过编译:

struct A{A() = delete;
  int t(){ return 1;}
}

int main(){decltype(A().t()) i; // BAD
}

因为 A() 是不存在的。

但改用 declval 就可能绕过问题了:

int main(){decltype(std::declval<A>().t()) i; // OK
}
纯虚类

在纯虚基类上有时候元编程会比拟麻烦,这时候可能能够借助 declval 来避开纯虚基类不能实例化的问题。

在第一个示例中有相应的参考 decltype(std::declval<Base<int>>().t()) b{}; // = int b;

Refs

  • C++ Type Deduction Introduction – hacking C++
  • std::declval – cppreference.com
  • decltype specifier – cppreference.com

Tricks

下面的代码波及到了一些习用法,上面做一简略的背景介绍,也蕴含一点点联想延长。

采纳一个一般的抽象类作为基类

模板类的体系设计中,如果基类的代码、数据很多,可能会导致收缩问题。一个解决办法是采纳一个一般的基类,并在其根底上建设模板化的基类:

struct base {virtual ~base_t(){}
  
  void operation() { do_sth(); }
  
  protected:
  virtual void do_sth() = 0;};

template <class T>
  struct base_t: public base{
    protected:
    virtual void another() = 0;};

template <class T, class C=std::list<T>>
  struct vec_style: public base_t<T> {
    protected:
    void do_sth() override {}
    void another() override {}
    
    private:
    C _container{};};

这样的写法,能够将通用逻辑(不用泛型化的)抽出到 base 中,防止留在 base_t 中随着泛型实例化而收缩。

纯虚类如何放入容器里

顺便也谈谈纯虚类,抽象类,的容器化问题。

对于类体系设计,咱们激励基类纯虚化,但这样的纯虚基类就无奈放到 std::vector 等容器中了:

#include <iostream>

namespace {struct base {};

  template<class T>
    struct base_t : public base {virtual ~base_t(){}
      virtual T t() = 0;};

  template<class T>
    struct A : public base_t<T> {A(){}
      A(T const& t_): _t(t_) {}
      ~A(){}
      T _t{};
      virtual T t() override { std::cout << _t << '\n'; return _t;}
    };
}

std::vector<A<int>> vec; // BAD

int main() {}

怎么破?

这里用 declval 是没意义的,应该应用智能指针来装璜形象基类:

std::vector<std::shared_ptr<base_t<int>>> vec;

int main(){vec.push_back(std::make_shared<A<int>>(1));
}

因为咱们为泛型类 base_t 申明了非泛型的基类 base,所以还可能采纳 std::vector<base> 的办法,但这要求你将所有 virtual 接口都抽取到 base 中,那样做的话,总会有一部分泛型接口无奈抽取,所以这种办法有可能是行不通的。

如果感觉虚函数与其重载如此苦楚居然不能忍的话,你能够思考 谈 C++17 里的 Builder 模式 所介绍的 CRTP 习用法的能力,CRTP 在模板类继承体系中是个很弱小的编译期多态能力。

除此而外,还能够放弃基类抽象化的设计方案,改用所谓的运行时多态 trick 来设计类体系。

Runtime Polymorphism

这是一种由 Sean Parent 提供的 运行时多态 编码技术:

#include <iostream>
#include <memory>
#include <string>
#include <vector>

class Animal {
 public:
  struct Interface {virtual std::string toString() const = 0;
    virtual ~Interface()                 = default;};
  std::shared_ptr<const Interface> _p;

 public:
  Animal(Interface* p) : _p(p) { }
  std::string toString() const { return _p->toString(); }
};

class Bird : public Animal::Interface {
 private:
  std::string _name;
  bool        _canFly;

 public:
  Bird(std::string name, bool canFly = true) : _name(name), _canFly(canFly) {}
  std::string toString() const override { return "I am a bird";}
};

class Insect : public Animal::Interface {
 private:
  std::string _name;
  int         _numberOfLegs;

 public:
  Insect(std::string name, int numberOfLegs)
      : _name(name), _numberOfLegs(numberOfLegs) {}
  std::string toString() const override { return "I am an insect.";}
};

int main() {
  std::vector<Animal> creatures;

  creatures.emplace_back(new Bird("duck", true));
  creatures.emplace_back(new Bird("penguin", false));
  creatures.emplace_back(new Insect("spider", 8));
  creatures.emplace_back(new Insect("centipede", 44));

  // now iterate through the creatures and call their toString()

  for (int i = 0; i < creatures.size(); i++) {std::cout << creatures[i].toString() << '\n';}
}

其特点是基类不是基类,基类的嵌套类才是基类:Animal::Interface 才是用于类体系的形象基类,它是纯虚的,但却不影响 std::vector<Animal> 的无效编译与工作。Animal 应用简略的转接技术将 Animal::Interface 的接口(如 toString())映射进去,这种转接有点像 Pimpl Trick,但也有一点渺小的区别。

后记

总的一句话,declval 就是专门治那些无奈实例化具体对象的场合的。

std::declval<T>() 也被典型地用在编译期测试等用处,下一次有闲再做探讨吧,那个话题太大了。

正文完
 0