If you recently watched CppCon 2014: Herb Sutter “Back to the Basics! Essentials of Modern C++ Style”, you’ll be enticed to add auto liberally. However, I’m going to be a party pooper.
If you use auto heavily, you need to know exactly what gets preserved and what gets discarded.
1. Type deduction drops the const
When using type deduction with references, the const is dropped.
#include <iostream>
int main()
{
const int maxRetries { 3 };
auto retries { maxRetries }; // const is not carried over
retries = 5;
std::cout << "maxRetries: " << maxRetries << '\\n';
std::cout << "retries: " << retries << '\\n';
}
// Terminal output:
maxRetries: 3
retries: 5
In this case, the const must be reapplied, const auto retries { maxRetries };.
However, this can become tedious and you can easily forget to do this.
2. Type deduction drops the reference (&)
In this example auto deduces to int, not int&.
#include <iostream>
// This functions returns a reference
int& activeThreshold()
{
static int threshold { 75 };
return threshold;
}
int main()
{
// Reference is not carried over
auto thresholdCopy { activeThreshold() };
thresholdCopy = 90;
std::cout << "copy: " << thresholdCopy << '\\n';
std::cout << "original: " << activeThreshold() << '\\n';
}
// Terminal output:
copy: 90
original: 75
If you would like to preserve the reference, simply add it in:
auto& thresholdCopy { activeThreshold() };
Top-level const and low-level const
Observe how in the example in Type deduction drops the const. This is because the const was a top-level const. This is defined as a const that’s applied on the object. The object is immutable.
// a top-level const is applied on the object DIRECTLY. E.g.
const int x { 11 };
// x = 22 is NOT ALLOWED
int* const ptr = &x;
// ptr is a const pointer.
A low-level const is applied to the pointer/reference, not the referenced type.
const int* ptr = &x; // A pointer to a const int
const int& x = y; // A reference to a const int
This is relevant since type deduction doesn’t apply to low-level const, however it strips the const from a high-level const as was demonstrated in the first example.
Type deduction and pointers
#include <iostream>
int main()
{
const int sensorLimit { 100 };
const int* limitPtr { &sensorLimit };
auto deducedPtr { limitPtr };
// Equivalent to auto*
std::cout << *deducedPtr << '\\n';
}
Terminal output:
100
The type of deducedPtr is:
const int*
The pointer itself is copied, but the low-level const is preserved.
That means this is not allowed:
*deducedPtr = 80; // error: pointed-to int is const
The reason that references are dropped during type deduction but pointers are not dropped is because references and pointers have different semantics. ↗️
decltype(auto) preserves the exact return category
Sometimes you want the exact type and value category of an expression.
That is what decltype(auto) is for.
#include <iostream>
int& activeThreshold()
{
static int threshold { 75 };
return threshold;
}
int main()
{
decltype(auto) thresholdRef { activeThreshold() };
thresholdRef = 95;
std::cout << activeThreshold() << '\\n';
}
Terminal output:
95
Because activeThreshold() returns int&, decltype(auto) preserves that exact type.
In Conclusion
Plain auto is a value declaration. It copies the initializer’s value, dropping references and top-level const.
Pointers are different: pointer-ness is preserved, and low-level const remains part of the deduced type.
The rule is simple: use plain auto when you want a value, auto& when you want a reference, const auto& when you want read-only access without copying, and auto* when you want the declaration to make pointer semantics visible.