Type Deduction Using decltype and declval

Learn to use decltype and std::declval to determine the type of an expression at compile time.

Ryan McCombe
Updated

In C++, we usually know the types we're working with. But in more advanced scenarios which we'll be working with soon, particularly with templates, we might not know a type ahead of time.

Instead, we might only know that we need the type that results from a specific expression, like calling a function. This is where the compiler's type deduction capabilities become incredibly useful.

This lesson introduces two tools for this purpose: decltype and std::declval. We'll start with decltype, a specifier that lets you ask the compiler, "What is the type of this expression?" It's a direct way to capture a type without needing to know it yourself.

However, using decltype can become cumbersome if the expression requires constructing an object, especially one with complex constructor arguments. We'll see how std::declval solves this problem. It allows us to work with a hypothetical instance of a type within a decltype expression, removing the need for actual object creation.

Using decltype

Occasionally, it will be easier (or necessary) to ask the compiler to figure out what type an expression returns. We can do this using the decltype specifier.

In the following examples, we use decltype simply to set up type aliases, but its applications are much wider ranging as we'll see through the rest of this chapter.

Below, 42 is an int, so decltype(42) will result in int:

int main() {
  // SomeType will be an alias for int
  using SomeType = decltype(42);

  SomeType SomeValue;
}

The expression we use with decltype does not need to be a simple literal - below, we use it to determine what is returned by a function:

int SomeFunction() { return 42; }

int main() {
  // SomeType will be an alias for int
  using SomeType = decltype(SomeFunction());

  // This will be an int
  SomeType SomeValue;
}

We don't need to use it with a type alias - we can use it almost anywhere a type is expected:

int SomeFunction() { return 42; }

int main() {
  // This will be an int
  decltype(SomeFunction()) SomeValue;
}

Limitations of decltype

In some scenarios, the expression we need to write to determine a type using decltype can be quite complex. For example, we might want to get the type returned by some member function. This requires us to construct an object just to see what its SomeFunction() method return type is:

struct MyType {
  int SomeFunction(){};
};

int main() {
  // Will be int
  using SomeType = decltype(
    MyType{}.SomeFunction()
  );
}

Things can get more awkward if the type isn't default-constructible. This means we now need to provide some random constructor arguments just to find out what type SomeFunction() returns:

struct MyType {
  MyType(int, float, bool) {}
  int SomeFunction(){};
};

int main() {
  // Will be int
  using SomeType = decltype(
    MyType{42, 9.8, true}.SomeFunction()
  );
}

Worse, in more complex scenarios involving templates that we'll cover later, we won't even know what type we're constructing, or what arguments its constructor requires.

To help with this, we have std::declval.

Using std::declval

std::declval is available by including <utility>, and it allows us to work with a hypothetical object of a specific type. The following examples are not directly useful, but they lay the foundations:

#include <utility>

struct SomeType {};

int main() {
  // A hypthetical int
  std::declval<int>();

  // A hypthetical float
  std::declval<float>();

  // A hypthetical SomeType
  std::declval<SomeType>();
}

This hypothetical object is only useful to assess the characteristics of its type at compile time, such as the types of its member variables, and the return types of its member functions.

The key advantage of std::declval over decltype is that it doesn't need to actually construct an object, so we don't need to provide constructor arguments. We can solve our original problem like this:

#include <utility>

struct MyType {
  MyType(int, float, bool) {}
  int SomeFunction(){};
};

int main() {
  // Before:
  using SomeType = decltype(
    MyType{42, 9.8, true}.SomeFunction()
  );
  
  // After:
  using SomeType = decltype(
    std::declval<MyType>().SomeFunction()
  );
}

Obviously, these examples are extremely contrived. We can clearly see that MyType::SomeFunction() returns an int, so we don't need this complexity.

However, the rest of this chapter will introduce scenarios where, at the time we're writing our code, we don't necessarily know what types we will be dealing with. In these scenarios, tools like decltype and std::declval become much more valuable.

Summary

This lesson introduced decltype and std::declval as tools for compile-time type deduction. We saw how decltype can determine the type of an expression, and how std::declval can simplify this process by providing a hypothetical object, avoiding the need for actual construction.

These are especially useful in generic programming contexts which we'll be introducing in the next chapter.

Key Takeaways:

  • decltype(expression) returns the type of the given expression without evaluating it.
  • It can be used to declare variables or create type aliases with a deduced type.
  • To get the return type of a member function, you often need an object instance.
  • std::declval<T>() creates a reference to a hypothetical object of type T, usable only within unevaluated contexts like decltype.
  • Using std::declval<MyType>().MemberFunction() inside decltype avoids the need to construct an object of MyType.
Next Lesson
Lesson 21 of 126

Class Templates

Learn how templates can be used to create multiple classes from a single blueprint

Have a question about this lesson?
Temporarily unavailable while we roll out updates. Back in a few days!