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
  • 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:

  1. You need a template parameter T: typename T
  2. Bind T by universal reference, also known as perfect forwarding reference: T&& t
  3. 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

Made with Slides.com