Rule(s)
- Traits in C++11 are the possibility of getting metadata
on “effective” types while
writing trait logic on anonymous types in generics.
The list of available traits reflects the way C++ moves from C++11
to C++20 including “recent” obsolescenses.
- C++11 comes with a compiler, which is an interpreter! More or less, preprocessing is now used
for evaluating expressions at compilation time thanks to the key
constexpr
construct.
Example Palindrome.Cpp.zip
// 'constexpr' works with 'static' only...
// 'constexpr' allows compile-time initialization:
constexpr static const std::pair<char, char> _Intervals[] = {std::pair<char, char>('a', 'z'), std::pair<char, char>('A', 'Z')};
constexpr static const int _Intervals_size = 2; // No need with C++17: 'std::size(_Intervals)'
Example Constexpr.Cpp.zip
template<int... Integers> // C++14
constexpr std::array<int, sizeof...(Integers)> create_array(std::integer_sequence<int, Integers...>) noexcept {
return std::array<int, sizeof...(Integers)> { Integers... }; // Aggregate initialization...
}
constexpr std::array<int, 20> My_array = ::create_array(std::make_integer_sequence<int, 20>{});
static_assert(My_array[19] == 19);
Rule(s)
- C++17 comes with a new construction, which is
constexpr if(my_condition)
Note that constexpr if
may profitably
replace std::enable_if
from C++11.
Example Constexpr.Cpp.zip
class Constexpr_example {
public:
constexpr static int N = 8;
};
template<int N> constexpr bool Is_prime() noexcept {
if constexpr (N < 2) return false; // C++17
if constexpr (N == 2) return true; // C++17
else { // This branch is discarded at compilation time when 'N == 2'
bool is_prime = true;
for (int i = 2; i < N; i++)
if (N % i == 0) {
is_prime = false;
break;
}
return is_prime;
}
}
static_assert(::Is_prime<2>()); // No compilation error
// static_assert(::Is_prime<Constexpr_example::N>()); // Compilation error since 'Constexpr_example::N == 8'
Rule(s)
- Computing data at compilation time by means of
constexpr
allows lower charge at execution start time in particular. This advantage must be balanced against
some resulting abstruse code that does not favor maintenance at all.
Example (naive approach) Constexpr_.Cpp.zip
constexpr int N = 10; // Must be initialized
constexpr int Native_array[N] = {}; // Must be initialized
constexpr int Primes_() {
int cursor = 0;
for (int i = 2; cursor != N; i++) { // Candidate prime numbers...
// Naive solution:
bool is_prime = true;
for (int j = 2; j < i; j++)
if (i % j == 0) {
is_prime = false;
break;
}
// if (is_prime)
// // Compilation error: assignment of read-only location:
// Native_array[cursor++] = i;
}
return 0; // Done...
}
int Return = Primes_(); // This code causes the compiler to crash in a random way!
// static_assert(::Native_array[0] == 2); // It fails!
constexpr std::array<int, N> Primes() {
std::array<int, N> primes = {}; // Must be initialized
int cursor = 0;
for (int i = 2; cursor != N; i++) { // Candidate prime numbers...
// Naive solution:
bool is_prime = true;
for (int j = 2; j < i; j++)
if (i % j == 0) {
is_prime = false;
break;
}
// if (is_prime)
// // Compilation error: call to non-'constexpr' function:
// primes[cursor++] = i;
return primes;
}
}
class Constexpr_example {
public:
constexpr static std::array<int, N> Primes = ::Primes();
};
Example (compilation-time code only!) Constexpr.Cpp.zip
constexpr int Not_prime = -1;
constexpr int As_prime(int n) {
if (n < 2) return ::Not_prime;
for (int i = 2; i < n; i++)
if (n % i == 0)
return ::Not_prime;
return n;
}
// 'std::index_sequence' and 'std::make_index_sequence' are specializations of
// 'std::integer_sequence' and 'std::make_integer_sequence' with 'T' = 'std::size_t'
/** Internal '_create_array' template function called by 'create_array' */
template<typename Function, std::size_t... Integers>
// Trailing return type thanks to 'auto':
constexpr/*[]*/ auto _create_array(Function f, std::index_sequence<Integers...>) ->
// Returned type:
std::array<std::invoke_result_t<Function, std::size_t>, sizeof...(Integers)> {
return { { f(Integers)... } }; // Aggregate initialization
}
template<int N, typename Function>
// Trailing return type based on 'auto':
constexpr/*[]*/ auto create_array(Function f) ->
// Returned type:
std::array<typename std::result_of<Function(std::size_t)>::type, N> {
// 'std::make_index_sequence<7>{}' -> '<0,1,2,3,4,5,6,7>' become 'Integers'
return _create_array<Function>(f, std::make_index_sequence<N>{});
}
constexpr std::array<int, Constexpr_example::N> Primes =
::create_array<Constexpr_example::N/*, decltype(&::As_prime)*/>(::As_prime);
static_assert(Primes[0] == ::Not_prime); // No compilation error
static_assert(Primes[1] == ::Not_prime); // No compilation error
static_assert(Primes[2] != ::Not_prime); // No compilation error
static_assert(Primes[3] != ::Not_prime); // No compilation error
Rule(s)
- C++17 allows
constexpr
lambda expressions as well.
Example Constexpr.Cpp.zip
int main() {
…
/** Code is put inside 'main' for illustration purpose only! */
// 'N' is made available in local scope ('main') for *CAPTURE*;
constexpr int N = Constexpr_example::N;
// 'constexpr' lambda expression:
constexpr auto my_lambda = [N /* Capture 'constexpr' only */]() {
if (::Is_prime<Constexpr_example::N>()) // '::Is_prime<N>()' *DOES NOT* work...
return ::As_prime(N); // '::As_prime(Constexpr_example::N)' *DOES* work...
return ::Not_prime;
};
static_assert(my_lambda() == ::Not_prime); // No compilation error since 'Constexpr_example::N == 8'
}
See also
Basic traits: std::is_class
, std::is_same
…
Rule(s)
- C++ provides directly usable traits (
#include <type_traits>
)
that can be helpful. There is a clear (often unspoken) reality behind traits:
while traits are powerful
tools in library programming based on generics, application programming that fosters
“business types”
has to sparingly use traits to the risk of showing some misconception or clumsy conception.
Example (std::is_same
)
SFINAE.Cpp.zip
std::cout << std::boolalpha << std::is_same<std::pair<const std::string, std::string>,
std::unordered_map<std::string, std::string >::value_type >::value << std::endl; // 'true'
Example SFINAE.Cpp.zip
'.h' file:
class ClassWith_toString {
public:
std::string toString() {return "Returned by 'toString'...";}
…
};
class ClassWithout_toString final {};
'.cpp' file:
std::cout << std::boolalpha;
std::cout << std::is_class<ClassWith_toString>::value << std::endl; // 'true'
std::cout << std::is_trivially_copyable<ClassWith_toString>::value << std::endl; // 'true'
std::cout << std::is_final<ClassWith_toString>::value << std::endl; // 'false'
std::cout << std::is_final<ClassWithout_toString>::value << std::endl; // 'true'
Reinforcing type checking
Rule(s)
- The principle behind traits and meta-programming
is the writing of code, which IS NOT functional from an execution viewpoint.
Example Meta_programming.Cpp.zip
// '.h' file:
#include <type_traits>
class Note {
virtual void initialization();
};
class Confidential_note : public Note {
void initialization() override;
};
template <typename T> // 'my_function' has to work with a polymorphic type...
// Return type is 'void' due to 'std::enable_if_t' inference succeeds:
typename std::enable_if_t<std::is_polymorphic_v<T> > my_function(T&&); // C++17 required
// '.cpp' file:
int main() {
::my_function(Note());
::my_function(Confidential_note());
// ::my_function("'std::string' is not a polymorphic type..."); // Compilation error...
}
Code adaptation
Rule(s)
- A key advantage of traits is the writing of
self-adaptive code in relation with anonymous types in generics.
Example Inheritance_genericity_gcc.Cpp.zip
template<typename Any> class Pointer_based_hash {
public:
// If 'std::is_pointer<Any>::value' is true then 'std::enable_if' has a public member 'typedef' type, which equals 'Return_type' else there is no member 'typedef'
template<typename Return_type = std::size_t> typename std::enable_if<std::is_pointer<Any>::value, Return_type>::type
operator()(const Any pointer) const {
std::hash < std::bitset<sizeof (Any)> > hash;
return hash(reinterpret_cast<uintptr_t> (pointer)); // Storage relies on memory address!
}
template<typename Return_type = std::size_t> typename std::enable_if<!std::is_pointer<Any>::value, Return_type>::type
operator()(const Any& any) const {
std::hash < std::bitset<sizeof (Any*)> > hash;
return hash(reinterpret_cast<uintptr_t> (&any)); // Storage relies on memory address!
}
};
template <typename T> class Garden {
private:
// 'Pointer_based_hash<T>' (as second anonymous type) acts as a functor (definition of 'operator()' required in 'Pointer_based_hash<T>'):
std::unordered_set <T, Pointer_based_hash<T> > _garden;
public:
Garden(const T&);
};
Substitution Failure Is Not An Error (SFINAE)
Rule(s)
- For short, generic programming make assumptions on anonymous type properties.
Injecting effective types
without such expected properties creates compilation errors in generic code itself! Worst still,
SFINAE is the case when specialization of generics hides errors…
Example (simple) Meta_programming.Cpp.zip
// '.h' file:
#include <type_traits>class A {
public:
// virtual ~A(); // 'A' *IS NOT* polymorphic!
using Internal_type = A; // For simplification, but stupid...
};
template <typename T> // 'my_function' has to work with a polymorphic type...
// Return type is 'void' due to 'std::enable_if_t' inference succeeds:
typename std::enable_if_t<std::is_polymorphic_v<T> > my_function(T&&) { std::cout << "Base ver." << std::endl; }; // C++17 required
// Specialization of 'my_function':
template <typename T> void my_function(typename T::Internal_type&&) { std::cout << "Specialization, SFINAE" << std::endl; };
// '.cpp' file:
int main() {
::my_function<A>(A()); // 'Specialization, SFINAE'
}
Example (complex) SFINAE.Cpp.zip
// '.h' file:
template <typename T> class Has_toString { // Inspiration: https ://gist.github.com/fenbf/d2cd670704b82e2ce7fd
private:
typedef wchar_t No; // 'sizeof(wchar_t) == 2'
static_assert(sizeof(No) == 2, "'sizeof(No) != 2', impossible!");
typedef char Yes; // 'sizeof(char) == 1'
static_assert(sizeof(Yes) == 1, "'sizeof(Yes) != 1', impossible!");
// Two following functions have *NO BODY* since they only play a role at *compile time* (uncomment for test)
// Versions are instantiated by the compiler according to *effective type* injected in 'Has_toString':
template <typename ...> static No _Test(...) /*{ std::string s = "Never executed..."; }*/;
/* '_Test(...)' is inferred matches to all parameters. It is then inferred when 'Yes _Test(decltype(&C::toString))' *IS NOT*
*/
template <typename C> static Yes _Test(decltype(&C::toString)); // Get the (returned) type of the 'toString' function via member function pointer!
/* '_Test(decltype(&C::toString))' is inferred since 'std::declval<std::nullptr_t>())' conforms to 'decltype(&C::toString)'
*/
public:
static std::string Get_metadata() {
std::string result = "decltype(std::declval<Yes>()): " + std::string(typeid(decltype(std::declval<Yes>())).name()) + '\n'
+ "decltype(_Test<T>(nullptr)): " + std::string(typeid(decltype(_Test<T>(nullptr))).name());
return result;
};
// 'T' plays the role of 'C' in 'Test', i.e., 'std::string' plays the role of 'T' in 'std::enable_if<Has_toString<T>::Result, std::string>'
static constexpr const bool Result = sizeof(_Test<T>(std::declval<std::nullptr_t>())) == sizeof(Yes);
// Simplified alternative ('nullptr' matches to the member function pointer '&C::toString'):
static constexpr const bool Result_ = std::is_same<decltype(_Test<T>(nullptr)),
// 'std::declval<Yes>()' returns a *rvalue ref.* whose type is 'Yes':
std::remove_reference_t<decltype(std::declval<Yes>())> >::value; // Remove '&&'
};
template<typename T>
// 'Has_toString<T>::Result' exprime une condition calculée "à la compilation"
// La fonction 'Call_toString' *n'est pas instanciable* du point de vue de la généricité si 'false' :
/* C++14: */ typename std::enable_if_t<Has_toString<T>::Result, std::string> // <=> 'std::enable_if<Has_toString<T>::Result, std::string>::type' in C++11
Call_toString(T* t) {
/* 'T' has 'toString' ... */
return t->toString();
}
std::string Call_toString(...) { // Version disponible (*non instanciée*) pour traiter 'false'
return "'toString()' method is undefined...";
}
// '.cpp' file:
class ClassWith_toString {
public:
std::string toString() {return "Returned by 'toString'...";}
…
};
class ClassWithout_toString final {};
…
std::cout << std::boolalpha << "Has_toString<ClassWith_toString>: " << Has_toString<ClassWith_toString>::Result_ << std::endl; // 'true'
std::cout << std::boolalpha << "Has_toString<ClassWithout_toString>: " << Has_toString<ClassWithout_toString>::Result << std::endl; // 'false'
std::cout << std::boolalpha << "Has_toString<int>: " << Has_toString<int>::Result << std::endl; // 'false'
“My effective type is itself a template class!”
Rule(s)
- Power of traits may lead us to clumsy code whose return of investment is debatable.
Example Trait.Cpp.zip
// '.h' file:
template<typename T> class My_template { /* Given a template class...*/ };
namespace V1 {
template<typename Realized, template<typename> class Realizer> class is_realization_of : public std::integral_constant<bool, false > /* <=> std::false_type */ {};
template<typename Realized, template<typename> class Realizer> class is_realization_of<Realizer<Realized>, Realizer> : public std::true_type {};
// 'My_template_realization' is indeed 'My_template<T>' for some 'T':
template<typename My_template_realization> class My_class {
// Caution: comma is misinterpreted by the pre-processor, requiring extra parentheses:
static_assert((is_realization_of<My_template_realization, My_template>::value), "failure");
};
}
namespace V2 { // For any number of (template) effective types...
template<template<typename...> class TT, typename T> class is_realizer_of : std::false_type {};
template<template<typename...> class TT, typename... Ts> class is_realizer_of<TT, TT<Ts...> > : std::true_type {};
template<typename My_template_realization> class My_class {
static_assert((is_realizer_of<My_template, My_template_realization>::value), "failure");
};
}
// '.cpp' file:
// V1::My_class<int> mc1; // 'static_assert' error since 'int' is not a template class!
V1::My_class<My_template<int> > mc2; // OK!
See also