std::declval
和 decltype
The picture is from C++ Type Deduction Introduction – hacking C++ but slightly altered to fit the banner
About the decltype
Decltype (expr) is a C++11 keyword that can be used to find the type of an entity or expression.
#include <iostream>
int main(a) {
int i = 33;
decltype(i) j = i * 2;
std::cout << j;
}
Copy the code
It’s so simple that it needs no explanation.
But how could something so simple require such a big deal as adding a keyword? It’s meta-programming! In the world of metaprogramming, a lifetime of dubious string of template class declarations can be overwhelming, and writing them repeatedly is even more cumbersome. For example, a run-time debug log output:
It’s not the longest name I can remember, just one of the easiest quotes to snag. There are plenty of examples.
Use fSM-CXX as an example to illustrate the use of decltype:
void test_state_meta(a) {
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(a);// ...
}
Copy the code
Obviously, using M = decltype(M) is more concise, especially when machine_t
> may be a super long string with super many template arguments defined. Decltype’s value will be even more obvious.
In metaprogramming, especially when large class systems are entangled with each other, many times we may not have to rely on the decltype capability and auto automatic derivation capability, because we may not be able to predict what the specific type will be in a specific scenario.
Normalized coding style
In addition, using decltype and using can make your code normative and effortless.
When writing a class, we should use the type aliasing capabilities provided by using, which may also involve using decltype.
The advantage of using is that you can explicitly urge the compiler ahead of time to derive related types, and if you have errors you can fix them in a set of using statements rather than in a bunch of code paragraphs to figure out why the type is wrong.
Using the wrong type can lead to a lot of forced rewriting.
Using can also help you reduce code section changes. For example, when using Container= STD ::list
is changed to using Container= STD ::vector
, your already written code paragraphs and the Container _container declaration can be changed without any changes. You just need to recompile.
This section does not refer to use cases, because that would be distracting. And it won’t help if I tell you now.
aboutstd::declval
STD ::declval
() returns an rvalue reference of type T.
But what CPPREf is saying is so confusing, what can Declval actually do? It is a forgery that returns a T object with an rvalue reference reference. In other words, it is equivalent to the compile-time state of the following objref:
T obj{};
T &objref = obj{};
Copy the code
First, it is lexically and semantically equivalent to objref. It is an instance value of object T and has type T&&. Second, it is only used in non-evaluation situations; Again, it doesn’t really exist. Is what meaning, said people in the mid-term, need a value object, but does not want the value object to be compiled into a binary entity, it is used to construct a declval virtual, so as to obtain a temporary object, can be applied on the object operation, such as calling a member function of what, but now that is virtual, There can’t really be such a temporary object, so I call it a pseudo instance.
We often do not really need the pseudo-instance obtained by declval evaluation directly, but more need to use this pseudo-instance to obtain the corresponding type description, that is, T. So in general, the declval is usually surrounded by the decltype calculation, trying to get T is our real purpose:
#include <iostream>
namespace {
struct base_t { virtual ~base_t(){} };
template<class T>
struct Base : public base_t {
virtual T t(a) = 0;
};
template<class T>
struct A : public Base<T> {
~A() {}virtual T t(a) override { std::cout << "A" << '\n'; returnT{}; }}; }int main(a) {
decltype(std::declval<A<int> > ().t()) a{}; // = int a;
decltype(std::declval<Base<int> > ().t()) b{}; // = int b;
std::cout << a << ', ' << b << '\n';
}
Copy the code
As we can see, the pseudo-instance of A
can “call” A’s member function t(), and then use decltype to get the return type of t() and declare A concrete variable A. Because t() returns type t, this variable declaration statement in main() 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
The core strength of declval(expr) is clearly shown in the above example: it does not actually evaluate expr. So you don’t have to generate any temporary objects in EXPR, and no real computation will occur because the expression is complex. This is very useful for complex metaprogramming environments.
The following page from one of the slides also shows a use case where an expression does not need to be evaluated but only evaluated:
FROM: HERE
But not only that, declval’s unrequited value gave rise to a further power.
There is no default constructor
If a class does not define a default constructor, it can be cumbersome in a metaprogramming environment. For example, the following decltype will not compile:
struct A{
A() = delete;
int t(a){ return 1; }}int main(a){
decltype(A().t()) i; // BAD
}
Copy the code
Because A(alpha) doesn’t exist.
But use declval to get around the problem:
int main(a){
decltype(std::declval<A>().t()) i; // OK
}
Copy the code
Pure virtual class
In the pure virtual base class sometimes metaprogramming will be more troublesome, this time may be able to use declval to avoid the pure virtual base class can not instantiate the problem.
Decltype (STD ::declval
>().t()) b{}; // = int b; .
Refs
- C++ Type Deduction Introduction – hacking C++
- std::declval – cppreference.com
- decltype specifier – cppreference.com
Tricks
The above code touches on some idioms, but here is a brief background, with a bit of associative extension.
Use a plain abstract class as the base class
In the template class architecture, if the base class code, data is too much, may lead to bloat problem. One solution is to take a generic base class and create a template-based base class on top of it:
struct base {
virtual ~base_t() {}void operation(a) { do_sth(a); }protected:
virtual void do_sth(a) = 0;
};
template <class T>
struct base_t: public base{
protected:
virtual void another(a) = 0;
};
template <class T.class C=std::list<T>>
struct vec_style: public base_t<T> {
protected:
void do_sth(a) override {}
void another(a) override {}
private:
C _container{};
};
Copy the code
Writing this way, you can extract generic logic (that doesn’t need to be genericized) into base, instead of remaining in Base_t bloated with generic instantiation.
How do pure virtual classes fit into containers
I’ll also talk about the containerization of pure virtual classes, abstract classes.
For class architecture, we encourage pure virtual base classes, but such pure virtual base classes cannot be placed in STD ::vector or other containers:
#include <iostream>
namespace {
struct base {};
template<class T>
struct base_t : public base {
virtual ~base_t() {}virtual T t(a) = 0;
};
template<class T>
struct A : public base_t<T> {
A() {}A(T const& t_): _t(t_) {}
~A(){}
T _t{};
virtual T t(a) override { std::cout << _t << '\n'; return _t; }}; } std::vector<A<int>> vec; // BAD
int main(a) {}Copy the code
How to break?
Instead of using declval, use smart Pointers to decorate abstract base classes:
std::vector<std::shared_ptr<base_t<int>>> vec;
int main(a){
vec.push_back(std::make_shared<A<int> > (1));
}
Copy the code
Since we declare a non-generic base class for the generic base_t, it is also possible to use STD ::vector
, but this would require you to extract all virtual interfaces into base. In that case, there will always be some generic interfaces that cannot be extracted. So it might not work.
If you can’t bear the pain of virtual functions and their overloading, consider the CRTP idiom capabilities introduced in C++17’s Builder mode, which is a powerful compile-time polymorphism in the template class inheritance system.
In addition, you can also abandon the abstract design of the base class and use the so-called run-time polymorphic trick to design the class system.
Runtime Polymorphism
This is a runtime polymorphic coding technique provided by Sean Parent:
#include <iostream>
#include <memory>
#include <string>
#include <vector>
class Animal {
public:
struct Interface {
virtual std::string toString(a) const = 0;
virtual ~Interface() = default;
};
std::shared_ptr<const Interface> _p;
public:
Animal(Interface* p) : _p(p) { }
std::string toString(a) 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(a) 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(a) const override { return "I am an insect."; }};int main(a) {
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(a); i++) { std::cout << creatures[i].toString() < <'\n'; }}Copy the code
Animal::Interface (STD ::vector
); Animal::Interface (STD ::vector
); Animal::Interface (STD ::vector
); Animal uses a simple transition technique to map Animal::Interface’s Interface (like toString()). This transition is a bit like the Pimpl Trick, but with a slight difference.
Afterword.
In a word, declval is for situations where it is impossible to instantiate a concrete object.
STD ::declval
() is also typically used in compile-time tests, etc.
:end: