std::declval and decltype

About decltype

decltype(expr) is a new keyword added to C++11 to type out entities or expressions.

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

It is simple and needs no additional explanation.

But how can something so simple require such a big thing as a new keyword? It’s metaprogramming! In the world of metaprogramming, a long string of template class declarations is crippling, and writing them repeatedly is even more tedious. For example, a runtime debug log output.

It’s not the longest name I can remember, just the one reference that I can intercept most easily. There are plenty of examples like this.

Here’s a slightly rewritten example to illustrate the usefulness of decltype.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
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) is more concise, especially when machine_t<my_state, void, payload_t<my_state>> may be a super-long string with a super-long definition of template arguments, the value of decltype becomes more obvious.

In metaprogramming, especially in cases where large class systems are tangled with each other, there are many times when the power of decltype and auto-derivation may not be available, because we may not be able to predict what the specific type will be in a specific scenario.

Standardized coding style

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

When writing a class, we should make more use of the type aliasing capabilities provided by using, which of course may also involve the use of decltype.

The advantage of using is that the compiler can be explicitly prompted to do the relevant type derivation in advance, and if there is a mistake, it can be fixed at a set of using statements, rather than having to go through a bunch of code paragraphs to figure out why the wrong type was used.

Using the wrong type can lead to a huge pile of code that has to be rewritten.

Using using can also help you reduce the number of code paragraphs you have to change. For example, if using Container=std::list<T> is changed to using Container=std::vector<T>, your already written code paragraphs and even the Container _container declaration can be changed without a single change, just by recompiling.

This section does not give reference examples, because that would take away from the main point. And the timing is not good enough to tell you about it.

About std::declval

std::declval<T>() is not much to say, it returns a right-valued reference of type T.

But cppref is really confusing, what does declval really do? It is used to return a fake instance of a T object with a right-valued reference. In other words, it is equivalent to the compile-time state of objref as follows.

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

First, it is lexically and semantically equivalent to objref, which is an instance value of object T and has the type T&&; second, it is only used in non-valued situations; and third, it doesn’t really exist. What it means, in human terms, is that at compile time, you need a value object, but you don’t want it to be compiled as a binary entity, so you construct one virtually with declval, as if you had a temporary object on which you could apply operations, such as calling member functions, but since it is virtual, there is no such temporary object, so I call it pseudo-instances.

We often don’t really need the pseudo-instance directly from the declval, but more from the pseudo-instance to get the corresponding type description, i.e., T. So in general, declval is often surrounded by decltype computation, and trying to get T is the real goal: the declval is the only one we need.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
#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';
}

As you can see, the pseudo-instance of A<int> can “call” A’s member function t(), and then with the help of decltype we can get the return type of t() and use it to declare a specific variable a. Since the return type of t() is T, this variable declaration in the main() function statement in the main() function is actually equivalent to int a{};.

This example is meant to help you understand what declval actually means; the example itself is rather meaningless.

The power of declval

The core power of declval(expr) is clear from the example above: it doesn’t really evaluate expr. So you don’t have to generate any temporary objects at expr, and no real computation occurs because the expression is complex. This is very useful for complex environments with metaprogramming.

The following page from a ppt also shows a use case where the expression does not have to be evaluated but only type.

But not only that, further power is derived from the undeclval of declval.

No default constructor

If a class does not define a default constructor, it can be troublesome in a metaprogramming environment. For example, the following decltype will not pass compilation.

1
2
3
4
5
6
7
8
struct A{
  A() = delete;
  int t(){ return 1; }
}

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

because A() is not present.

But by using declval instead, you can get around the problem:

1
2
3
int main(){
  decltype(std::declval<A>().t()) i; // OK
}
Pure Virtual Class

Sometimes metaprogramming on purely virtual base classes can be tricky, and it may be possible to bypass the problem of purely virtual base classes not being instantiated with the help of declval.

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

Refs

Tricks

The above code involves some of the usual method, the following is a brief background, but also contains a little association extension.

Use a common abstract class as the base class

The system design of template classes may lead to bloat problems if the base class has a lot of code and data. One solution is to take a common base class and build templated base classes on top of it

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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 is written in such a way that generic logic (that doesn’t have to be generalized) can be abstracted out into base and avoids being left in base_t to bloat with generic instantiation.

How to put a pure virtual class in a container

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

For class system design, we encourage pure virtualization of base classes, but such pure virtual base classes cannot be put into containers such as std::vector.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#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() {
}

What to do?

It doesn’t make sense to use declval here, you should use smart pointers to decorate the abstract base class with.

1
2
3
4
5
std::vector<std::shared_ptr<base_t<int>>> vec;

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

Since we declare a non-generic base class base for the generic class base_t, it is also possible to use the std::vector<base> approach, but this requires you to extract all virtual interfaces into base, which would always leave some of the generic interfaces unextracted, so there is a chance that this approach will not work.

If you find virtual functions and their overloading so painful that you can’t stand them, you can consider CRTP, which is a very powerful compile-time polymorphism capability in the template class inheritance system.

In addition, it is possible to abandon the base class abstraction scheme and design the class system with the so-called runtime polymorphic trick.

Runtime Polymorphism

This is a runtime polymorphism coding technique provided by Sean Parent.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
#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 is the abstract base class used for the class system, it is purely imaginary but does not affect the valid compilation and working of std::vector<Animal>. Animal uses a simple transfer technique to map the interface of Animal::Interface ( Such as toString()), which is a bit like Pimpl Trick, but with a slight difference.

Postscript

In a nutshell, declval is specifically for situations where concrete objects cannot be instantiated.

std::declval<T>() is also typically used for compile-time testing and other purposes, so we’ll explore that next time we have time, it’s too big a topic.