C++ metaprogramming by David A.
Specilization: Implicit vs Explicit Instantiation
Explicit:
We want to tell compiler where, when and how.
Why -> bigger template , so we want all instanation in one inst unit and reference to other inst unit.
There must be only copy instantiation (exactly one definition of corresponding specialization) in all inst units,
use declaration in other Instantiation
template class vector<foo> //defination
template class vector<foo, my_allocator<foo>>; //filled with actual parameter we want
template void swap(foo>(foo&, foo&));
template void swap(bra&, bar&);
//individual member function can be explicity instantiated
template class vector<foo, my_allocator<foo>>::push_back(foo const&);
//C++03/98
float a=2.5f;
std::vector<std::pair<float,float>>::iterator itr=vec.begin();
//C++11
auto itr=vec.begin();
const int i;
auto ii=i;
const int j& = i;
auto y=j;
int&& z=0;
auto z1=std::move(z);
// no easy way to return type of template
template <typename T1, template T2>
T1/T2 find(T2 const &a, T2 const &b) {
return a*b;
}
decltype(e)
xvalue/prvalue are forms of rvalues. We do not require you to know this.
Non-simplified set of rules can be found here.
https://www.oreilly.com/library/view/effective-modern-c/9781491908419/ch01.html
Examples include:
decltype(fun()) value =x;
const int i;
auto ii=i; // int
decltype(i) x; //const int
decltype(auto) x=i; // const int
const int j& = i;
auto y=j; //int
decltype(j) =i ; // int& - lvalue
decltype(auto) j=i;
decltype((j)) k ; // not initialized
decltype(j+i) b; // not initialized ?? error or not ??
int *ptr;
decltype(*ptr) c; // yeild reference of p, hence c is int& so must be initalized //error?
decltype(5) z; // int - prvalue
int&& z=0;
auto z1=std::move(z); //int
decltype(auto) z2=std::move(z); //int&&
Iterator used over templated collection and returns a reference to an item at a particular index
template <typename It>
??? find(It beg, It end, int index) {
for (auto it = beg, int i = 0; beg != end; ++it; ++i) {
if (i == index) {
return *it;
}
}
return end;
}
We know the return type should be decltype(*beg), since we know the type of what is returned is of type *beg
This will not work, as beg is not declared until after the reference to beg
template <typename It>
decltype(*beg) find(It beg, It end, int index) {
for (auto it = beg, int i = 0; beg != end; ++it; ++i) {
if (i == index) {
return *it;
}
}
return end;
}
Introduction of C++11 Trailing Return Types solves this problem for us
template <typename It>
auto find(It beg, It end, int index) -> decltype(*beg) {
for (auto it = beg, int i = 0; beg != end; ++it, ++i) {
if (i == index) {
return *it;
}
}
return end;
}
Use auto and decltype (C+14): decltype(auto) with no trailing return type
template<class T1, class T2>
T1 multiplication(T1 const &a, T2 const &b) //T1/T2??
{
return a*b;
}
template<class T1, class T2>
auto multiplication(T1 const &a, T2 const &b) -> decltype(a*b)
{
return a*b;
}
//decltype of parenthesized variable is always a reference
decltype(i) a; //a type of i // only ref if i if is ref
decltype((i)) a; //compiler evaluate the operrand as expression
//decltype((i)) a; d is an int& so require initlization
template<typename T, typename U>
UNKNOWN func(T&& t, U&& u){ return t + u; };
//C++11
template<typename T, typename U>
auto myFunc(T&& t, U&& u) -> decltype (t+u))
{ return t+u };
//C++14
template<typename T, typename U>
decltype(auto) myFunc(T&& t, U&& u)
{ return t+u; }
int x = 42;
std::vector<decltype(x)> v(100, x); // v is a vector<int>
struct S {
int x = 42;
};
const S s;
decltype(s.x) y; // y has type int, even though s.x is const
int f() { return 42; }
int& g() { static int x = 42; return x; }
int x = 42;
decltype(f()) a = f(); // a has type int
decltype(g()) b = g(); // b has type int&
decltype((x)) c = x; // c has type int&, since x is an lvalue
fn_A(int i)
{
return i;
}
decltype(auto)
fn_B(int i)
{
return (i);
}
//I can convert the expression to int&,
//I’ll do so regardless of how i was actually declared.”
should have been written to return auto instead of decltype(auto)
decltype vs typeid
A number of add, remove, and make functions exist as part of type traits that provide an ability to transform types
#include <iostream>
#include <type_traits>
template<typename T1, typename T2>
auto print_is_same() -> void {
std::cout << std::is_same<T1, T2>() << "\n";
}
auto main() -> int {
std::cout << std::boolalpha;
print_is_same<int, int>();
// true
print_is_same<int, int &>(); // false
print_is_same<int, int &&>(); // false
print_is_same<int, std::remove_reference<int>::type>();
//remove_const<int> -> int
// remove_const<const int> -> int
//remove_const<const volatile int> -> volatile int
//remove_const< int*> -> int*
//remove_const<const int*> -> const int* // no top level const to remove
template<template T>
struct remove_const:typeIdenfity<T>{};
template<template T>
struct remove_const<T const>: TypeIdentify<T>{}; //specialized tempelate
// true
print_is_same<int, std::remove_reference<int &>::type>(); // true
print_is_same<int, std::remove_reference<int &&>::type>(); // true
print_is_same<const int, std::remove_reference<const int &&>::type>(); // true
}
demo850-transform.cpp
Remove_const: The member typedef type names the same type as of T that any top-level const-qualifier has been removed
#include <iostream>
#include <type_traits>
auto main() -> int {
using A = std::add_rvalue_reference<int>::type;
using B = std::add_rvalue_reference<int&>::type;
using C = std::add_rvalue_reference<int&&>::type;
using D = std::add_rvalue_reference<int*>::type;
std::cout << std::boolalpha
std::cout << "typedefs of int&&:" << "\n";
std::cout << "A: " << std::is_same<int&&, A>>::value << "\n";
std::cout << "B: " << std::is_same<int&&, B>>::value << "\n";
std::cout << "C: " << std::is_same<int&&, C>>::value << "\n";
std::cout << "D: " << std::is_same<int&&, D>>::value << "\n";
}
Since C++14/C++17 you can use shortened type trait names.
#include <iostream>
#include <type_traits>
auto main() -> int {
//using A = std::add_rvalue_reference<int>::type;
using A = std::add_rvalue_reference<int>;
using B = std::add_rvalue_reference<int&>;
std::cout << std::boolalpha
std::cout << "typedefs of int&&:" << "\n";
std::cout << "A: " << std::is_same<int&&, A>>::value << "\n";
std::cout << "B: " << std::is_same<int&&, B>>::value << "\n";
}
lvalue | const lvalue | rvalue | const rvalue | |
---|---|---|---|---|
template T&& | Yes | Yes | Yes | Yes |
T& | Yes | |||
const T& | Yes | Yes | Yes | Yes |
T&& | Yes |
Note:
Arguments
Parameters
#include <iostream>
auto print(std::string const& a) -> void {
std::cout << a << "\n";
}
auto goo() -> std::string const {
return "C++";
}
auto main() -> int {
auto j = std::string{"C++"};
auto const& k = "C++";
print("C++"); // rvalue
print(goo()); // rvalue
print(j); // lvalue
print(k); // const lvalue
}
#include <iostream>
template<typename T>
auto print(T&& a) -> void {
std::cout << a << "\n";
}
auto goo() -> std::string const {
return "Test";
}
auto main() -> int {
auto j = int{1};
auto const& k = 1;
print(1); // rvalue, foo(int&&)
print(goo()); // rvalue foo(const int&&)
print(j); // lvalue foo(int&)
print(k); // const lvalue foo(const int&)
}
demo851-bind1.cpp
demo852-bind2.cpp
lvalue | const lvalue | rvalue | const rvalue | |
---|---|---|---|---|
template T&& | Yes | Yes | Yes | Yes |
T& | Yes | |||
const T& | Yes | Yes | Yes | Yes |
T&& | Yes |
using A = std::add_rvalue_reference<int>::type;
using B = std::add_rvalue_reference<int&>::type;
using C = std::add_rvalue_reference<int&&>::type;
using D = std::add_rvalue_reference<int*>::type;
A& & becomes A&
A& && becomes A&
A&& & becomes A&
A&& && becomes A&&
http://thbecker.net/articles/rvalue_references/section_08.html
int n;
int& lvalue = n; // Lvalue reference
int&& rvalue = std::move(n); // Rvalue reference
template <typename T> T&& universal = n; // This is a universal reference.
auto&& universal_auto = n; // This is the same as the above line.
template<typename T>
void f(T&& param); // Universal reference
template<typename T>
void f(std::vector<T>&& param); // Rvalue reference (read the rules carefully).
If a variable or parameter is declared to have type T&& for some deduced type T, that variable or parameter is a forwarding reference (AKA universal reference in some older texts).
For more details on forwarding references, see this blog post
Attempt 1: Take in a value
What's wrong with this?
template <typename T>
auto wrapper(T value) {
return fn(value);
}
template <typename T>
auto wrapper(T const& value) {
return fn(value);
}
Attempt 2: Take in a const reference
What's wrong with this?
What happens if we pass in an rvalue?
// Calls fn(x)
// Should call fn(std::move(x))
wrapper(std::move(x));
What happens if wrapper needs to modify value?
Code fails to compile
template <typename T>
auto wrapper(T& value) {
return fn(value);
}
Attempt 3: Take in a mutable reference
What's wrong with this?
What happens if we pass in a const object?
What happens if we pass in an rvalue?
const int n = 1;
wrapper(n);
wrapper(1)
void add(int &a, int const b){a+=b;}
template<typename A, typename B, typename C>
void call(A a, B &b, C &c)
{
a(b,c);
}
int main()
{
int a=0, b=0;
call(Add,a,b);
call(add,a,1); // pass lvalue and rvalue
call(add,1,a); //expect two parameters to be lvalue ref.
//expect two parameters to be lvalue ref.
//compile time error
}
template <typename T>
auto wrapper(T&& value) {
return fn(value);
}
Attempt 4: Forwarding references
What's wrong with this?
// Instantiation generated
template <>
auto wrapper<int&>((int&)&& value) {
return fn(value);
}
// Collapses to
template <>
auto wrapper<int&>(int& value) {
return fn(value);
}
int i;
wrapper(i);
// Instantiation generated
auto wrapper<int&&>((int&&)&& value) {
return fn(value);
}
// Collapses to
auto wrapper<int&&>(int&& value) {
return fn(value);
}
int i;
wrapper(std::move(i));
Calls fn(i)
Also calls fn(i)
The parameter is an rvalue, but inside the function, value is an lvalue
Attempt 4: Forwarding references
We want to generate this
// We want to generate this.
auto wrapper<int&>(int& value) {
return fn(static_cast<int&>(value));
}
// We want to generate this
auto wrapper<int&&>(int&& value) {
return fn(static_cast<int&&>(value));
}
It turns out there's a function for this already
template <typename T>
auto wrapper(T&& value) {
return fn(std::forward<T>(value));
// Equivelantly (don't do this, forward is easier to read).
return fn(static_cast<T&&>(value));
}
#include <utility>
struct MyType{
MyType(int, double, bool){};
};
template <typename T, typename Arg>
T createT(Arg&& arg){
return T(std::forward<Arg>(arg));
}
int main(){
int lvalue{2020};
//std::unique_ptr<int> uniqZero = std::make_unique<int>(); // (1)
auto uniqEleven = createT<int>(2011); // (2)
auto uniqTwenty = createT<int>(lvalue); // (3)
//auto uniqType = std::make_unique<MyType>(lvalue, 3.14, true); // (4)
}
he three parts of the pattern to get perfect forwarding are:
template <typename T, typename... Args>
auto make_unique(Args&&... args) -> std::unique_ptr<T> {
// Note that the ... is outside the forward call, and not right next to args.
// This is because we want to call
// new T(forward(arg1), forward(arg2), ...)
// and not
// new T(forward(arg1, arg2, ...))
return std::unique_ptr(new T(std::forward<Args>(args)...));
//fn(std::forward<T>(value));
}
template <typename ...Params>
void f(Params&&... params)
{
y(std::forward<Params>(params)...);
}
#include <utility>
struct MyType{
MyType(int, double, bool){};
};
template <typename T, typename ... Args>
T createT(Args&& ... args){
return T(std::forward<Args>(args) ... );
}
int main(){
int lvalue{2020};
int uniqZero = createT<int>(); // (1)
auto uniqEleven = createT<int>(2011); // (2)
auto uniqTwenty = createT<int>(lvalue); // (3)
auto uniqType = createT<MyType>(lvalue, 3.14, true); // (4)
}
#include <utility>
struct MyType{
MyType(int, double, bool){};
};
template <typename T, typename Arg>
T createT(Arg&& arg){
return T(std::forward<Arg>(arg));
}
int main(){
int lvalue{2020};
//std::unique_ptr<int> uniqZero = std::make_unique<int>(); // (1)
auto uniqEleven = createT<int>(2011); // (2)
auto uniqTwenty = createT<int>(lvalue); // (3)
//auto uniqType = std::make_unique<MyType>(lvalue, 3.14, true); // (4)
}
The only real use for std::forward is when you want to wrap a function with a parameterized type. This could be because: