1
头图

std::declval and decltype

The picture of the title is from C++ Type Deduction Introduction-hacking C++ but slightly deformed to fit the banner

About decltype

decltype(expr) is a new keyword added to C++11. Its function is to find the type of entity or expression.

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

It is simple and requires no additional explanation.

But for such a simple thing, why do you need to add a keyword for such a big thing? It's still meta programming! In the world of meta-programming, a long list of template class declarations that doubt life can make people break down, and repetitive writing of them is even more cumbersome. For example, a run-time debug log output:

image-20211018201558979

This is not the longest name in my mind, but a quote that can be easily intercepted. There are many such examples.

Borrowing my talk about the second state mode in C++17, which is also fsm-cxx , is slightly rewritten to reflect the usefulness of 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();
  // ...
}

Obviously, using M = decltype(m) more concise, especially when machine_t<my_state, void, payload_t<my_state>> may be a super long string with super many template parameter definitions, the value of decltype will be more obvious.

In meta-programming, especially when it comes to the entanglement of large-scale class systems, many times may have to rely on the ability of decltype and the ability of auto to automatically derive, because in a specific scenario, we may not be able to preset what the specific type will be.

Standardized coding style

In addition, making good use of decltype and using can contribute to your code standardization and coding effort.

When writing a class, we should use the type alias capabilities provided by using more, of course, at the same time this may involve the use of decltype.

The advantage of using using is that the compiler can be explicitly urged to deduce related types in advance, and if there is an error, it can be corrected at a set of using statements, instead of studying why the type is incorrect in a lot of code paragraphs.

Using the wrong type may cause a large amount of code to be rewritten.

Using using can also help you reduce code paragraph changes. For example using Container=std::list<T> changed to using Container=std::vector<T> , your already written code paragraphs and even the Container _container statement can be unchanged at all, and only need to be recompiled.

This section does not give reference use cases, because that would be overwhelming. And if the time is not there, it won't work to tell you.

std::declval

std::declval<T>() There is nothing to say, it can return the rvalue reference of type T.

But cppref is really talking about is in the clouds. What exactly can declval do? It is used to return a fake instance of a T object, and at the same time has an rvalue reference. In other words, it is equivalent to the following compile-time state of objref:

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

First, it is lexically and semantically equivalent to objref, which is an instance value of the object T and has the type of T&&; second, it is only used for non-evaluation occasions; again, it does not really exist. What do you mean, it means that you need a value object during compilation, but you don’t want this value object to be compiled into a binary entity. Then use declval to construct one virtually, so as to get a temporary object. Perform operations on the object, such as calling member functions, but since it is virtual, there will not really be such a temporary object, so I call it a pseudo-instance.

We often don't really need the pseudo-instance obtained by declval evaluation directly, but we need to use this pseudo-instance to get the corresponding type description, that is, T. Therefore, in general, declval is often surrounded by decltype calculations. Trying to get T is our real goal:

#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';
}

It can be seen that A<int> can "call" the member function t() of A, and then with the help of decltype we can get the return type of t() and use it to declare a specific variable a. Because the return type of t() is T, this variable declaration statement in the main() function is actually equivalent to int a{}; .

This example is to help you understand the actual meaning of declval, the example itself is relatively meaningless.

The power of declval

declval(expr) is shown in the above example very clearly: it does not really evaluate expr. So you don't have to generate any temporary objects at expr, and no real calculations will occur because the expression is very complicated. This is very useful for the complex environment of metaprogramming.

The following page from a ppt also shows a use case where expressions do not need to be evaluated but only type:

slide 14

FROM: HERE

But not only that, the non-evaluation of declval also derives further power.

No default constructor

If a class does not define a default constructor, it may be very troublesome in a meta-programming environment. For example, the following decltype cannot be compiled:

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

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

Because A() does not exist.

But using declval instead can circumvent the problem:

int main(){
  decltype(std::declval<A>().t()) i; // OK
}
Pure virtual class

Sometimes metaprogramming can be troublesome on pure virtual base classes. At this time, declval may be used to avoid the problem that pure virtual base classes cannot be instantiated.

In the first example there is a corresponding reference decltype(std::declval<Base<int>>().t()) b{}; // = int b; .

Refs

Tricks

The above code involves some idioms. Here is a brief background introduction, which also includes a little bit of association extension.

Use an ordinary abstract class as the base class

In the system design of template classes, if the base class has a lot of code and data, it may cause expansion problems. One solution is to use a common base class and build a templated base class on top of it:

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{};
  };

This way of writing can extract the general logic (which does not need to be generic) into base, and avoid staying in base_t and expanding with the instantiation of generics.

How to put the pure virtual class in the container

By the way, also talk about the containerization of pure virtual classes, abstract classes, and so on.

For class system design, we encourage pure virtual base classes, but such pure virtual base classes cannot be placed in containers such as 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() {
}

How to break?

It is meaningless to use declval here. Smart pointers should be used to decorate the abstract base class:

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

int main(){
  vec.push_back(std::make_shared<A<int>>(1));
}
Since we have declared a non-generic base class base for the generic class base_t, we may also use std::vector<base> , but this requires you to extract all virtual interfaces into base. If you do that, there will always be some generic interfaces. Unable to extract, so this method may not work.

If you feel that virtual functions and their overloading are so painful and unbearable, you can consider idioms introduced by the Builder mode in C++17. CRTP is a very powerful compile period in the template class inheritance system. Polymorphism.

In addition, you can abandon the abstract design scheme of the base class and use the so-called run-time polymorphic trick to design the class system.

Runtime Polymorphism

runtime polymorphic encoding technology provided by 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';
  }
}

Its characteristic is that the base class is not the base class, the nested class of the base class is the base class: Animal::Interface is the abstract base class used in the class system, it is pure virtual, but it does not affect the effective compilation and compilation of std::vector<Animal> Work. Animal uses a simple transfer technique to map the Animal::Interface interface (such as toString()). This transfer is a bit like Pimpl Trick, but there is a slight difference.

postscript

In a word, declval is specifically for those occasions where it is impossible to instantiate specific objects.

std::declval<T>() also typically used for compile-time testing and other purposes. Let's discuss it next time you have free time. That topic is too big.

:end:


hedzr
95 声望19 粉丝