COMP6771

Advanced C++ Programming

Week 8.2

Advanced Types

decltype

decltype(e)

  • Semantic equivalent of a "typeof" function for C++
  • 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, result is T&
  • Rule 3: if e is an xvalue, result is T&&
  • Rule 4: if e is a prvalue, result is T

 

Non-simplified set of rules can be found here.

decltype

Examples include:

int i;
int j& = i;

decltype(i) x; // int - variable
decltype((i)) z; // int - lvalue
decltype(j) y; // int& - variable
decltype(5);   // int - prvalue

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;
}

Type Transformations

A number of add, remove, and make functions exist as part of type traits that provide an ability to transform types

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>();
	// 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

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>

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>;
    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&& represents everything!
    • template <typename T> void foo(T&& a);

Arguments

Parameters

Examples

#include <iostream>

auto print(const std::string& a) -> void{
	std::cout << a << "\n";
}

auto goo() -> std::string const {
	return "C++";
}

auto main() -> int {
	std::string j = "C++";
	std::string 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() -> int const {
	return 5;
}

auto main() -> int {
	int j = 1;
	int 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

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)

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&&

Forwarding functions

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

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));
}

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)...));
}
  • 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

 

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.
Made with Slides.com