COMP6771
Advanced C++ Programming
Week 8.2
Advanced Types
C++ metaprogramming by David A.
Specilization: Implicit vs Explicit Instantiation
- When a compiler sees the use of specialization it will create the specialization template argument for parameters.
- Compiler decide where, when and how much of specilization is need to be created.
- For class, it may not create all the members function of class
- Compiler might not generate non-virutal members or static data member.
- void f() { vector<int> vec{1,2,3};
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;
}
auto type deduction
decltype
decltype(e)
- Semantic equivalent of a "typeof" & "auto" function for C++
- type deduced from expression (return type) but dont initialize and evaluate
-
Rule 1:
-
If expression e is any of:
- variable in local scope
- variable in namespace scope
- static member variable
- function parameters
- then result is variable/parameters type T
-
If expression e is any of:
- Rule 2: if e is an lvalue (i.e. reference), result is T&
- Rule 3: if e is an xvalue, result is T&&
- Rule 4: if e is a prvalue, result is T
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
decltype
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&&
Determining return types
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
Determining return types
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
- to declare a template function whose return type depends on the types of its template arguments.
- to declare a template function that wraps a call to another function, and then returns the return type of the wrapped function.
M0re Examples
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
- Decltype gives the type information at compile time while typeid gives at runtime.
- So, if we have a base class reference (or pointer) referring to (or pointing to) a derived class object, the decltype would give type as base class reference (or pointer, but typeid would give the derived type reference (or pointer).
Type Transformations
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
Type Transformations
Type Transformations
#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";
}
Shortened Type Trait Names
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";
}
Binding
lvalue | const lvalue | rvalue | const rvalue | |
---|---|---|---|---|
template T&& | Yes | Yes | Yes | Yes |
T& | Yes | |||
const T& | Yes | Yes | Yes | Yes |
T&& | Yes |
Note:
- const T& binds to everything!
-
template T&& can by binded to by everything!
- template <typename T> void foo(T&& a);
Arguments
Parameters
Examples
#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
Forwarding references
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
Forwarding functions
Attempt 1: Take in a value
What's wrong with this?
- What if we pass in a non-copyable type?
- What happens if we pass in a type that's expensive to copy
template <typename T>
auto wrapper(T value) {
return fn(value);
}
Forwarding functions
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
Forwarding functions
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
}
Interlude: Reference collapsing
- An rvalue reference to an rvalue reference becomes (“collapses into”) an rvalue reference.
- All other references to references (i.e., all combinations involving an lvalue reference) collapse into an lvalue reference.
- T& & -> T&
- T&& & -> T&
- T& && -> T&
- T&& && -> T&&
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
Forwarding functions
Forwarding functions
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:
- You need a template parameter T: typename T
- Bind T by universal reference, also known as perfect forwarding reference: T&& t
- Invoke std::forward on the argument: std::forward<T>(t)
std::forward and variadic templates
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));
}
-
Often you need to call a function you know nothing about
- It may have any amount of parameters
- Each parameter may be a different unknown type
- Each parameter may be an lvalue or rvalue
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)
}
uses of std::forward
The only real use for std::forward is when you want to wrap a function with a parameterized type. This could be because:
-
You want to do something else before or after
- std::make_unique / std::make_shared need to wrap it in the unique/shared_ptr variable
- A benchmarking library might wrap a function call with timers
-
You want to do something slightly different
- std::vector::emplace uses uninitialised memory construction
-
You want to add an extra parameter (eg. always call a function with the last parameter as 1)
- This isn't usually very useful, because it can be achieved with std::bind or lambda functions.
Feedback
COMP6771 22T2 - 8.2 - Advanced Types
By imranrazzak
COMP6771 22T2 - 8.2 - Advanced Types
- 225