trainings/AdvancedCppV2/Presentation/template_deduction_guides.md

10 KiB
Raw Blame History

Deduction guides


Template deduction

There are three different scenarios during template type deduction:

  • Handle by reference or pointer: T& or T* with or without const or volatile
  • Handle by value: T with or without const or volatile
  • Handle by universal reference: T&& can't use const or volatile here.

Pass by reference


template <typename T>
void function(T& arg) {}

template <typename T>
void constFunction(const T& arg) {}

void foo(int) {}

int main() {
    int a = 4;
    const int b = 5;
    const int& c = a;
    int arr[] = {1,2};

    function(a);        // T -> int | arg -> int&
    function(5);        // Not compile!
    function(b);        // T -> const int | arg -> const int&
    function(c);        // T -> const int | arg -> const int&
    function(foo);      // T -> void(int) | arg -> void(&)(int)
    function(arr);      // T -> int[2] | arg -> int(&)[2]
    constFunction(a);   // T -> int | arg -> const int&
    constFunction(5);   // T -> int | arg -> const int&
    constFunction(b);   // T -> int | arg -> const int&
    constFunction(c);   // T -> int | arg -> const int&
    constFunction(foo); // T -> void(int) | arg -> const void(&)(int)
    constFunction(arr); // T -> int[2] | arg -> const int(&)[2]
}

Pass by value


template <typename T>
void function(T arg) {}

void foo(int) {}

int main() {
    int a = 4;
    const int b = 5;
    const int& c = a;
    int arr[] = {1,2};
    char name[] = "Mateusz";
    const char* str = name;
    const char* const ptr = name;

    function(a);        // T -> int | arg -> int
    function(5);        // T -> int | arg -> int
    function(b);        // T -> int | arg -> int
    function(c);        // T -> int | arg -> int
    function(foo);      // T -> void(*)(int) | arg -> void(*)(int))
    function(arr);      // T -> int* | arg -> int*
    function(str);      // T -> const char* | arg -> const char*
    function(ptr);      // T -> const char* | arg -> const char*
}

Pass by universal reference


template <typename T>
void function(T&& arg) {}

void foo(int) {}

int main() {
    int a = 4;
    const int b = 5;
    const int& c = a;
    int arr[] = {1,2};
    char name[] = "Mateusz";
    const char cstr[] = "Mateusz";
    const char* str = name;
    const char* const ptr = name;

    function(a);              // T -> int& | arg -> int&
    function(5);              // T -> int | arg -> int&&
    function(b);              // T -> const int& | arg -> const int&
    function(c);              // T -> const int& | arg -> const int&
    function(foo);            // T -> void(&)(int) | arg -> void(&)(int))
    function(arr);            // T -> int(&)[2] | arg -> int(&)[2]
    function(cstr);           // T -> const char(&)[8] | arg -> const char(&)[8]
    function(str);            // T -> const char(*&) | arg -> const char(*&)
    function(std::move(str)); // T -> const char(*) | arg -> const char(*&&)
    function(ptr);            // T -> const char(*const &) | arg -> const char(*const &)
}

Pass by universal reference - special treatment

When template parameter gets argument by universal reference, deducted type T doesn't remove the reference for l-values. In other words: r-values are treated as they are passed by value, but l-values are treated as a reference.

This is partially true. Scott Meyers said this is an abstraction layer. The real truth is reference collapsing:

  • T& & -> T&
  • T& && -> T&
  • T&& & -> T&
  • T&& && -> T&&

auto deduction

auto deduction works similar to templates, but there is one exception, which you should remember from previous slajds.

auto val = 5;

is equal to

template <typename T>     
void foo(T val);
const auto& val = 5;

is equal to

template <typename T>     
void foo(const T& val);
auto&& val = 5;

is equal to

template <typename T>     
void foo(T&& val);
___

auto deduction - one exception

template <typename T>
void foo(T t) {}

auto val = {1, 2, 3, 4}; // std::initializer_list<int>
foo({1, 2, 3, 4}); // deduction failed!

Need to explicity use initializer_list

template <typename T>
void foo(std::initializer_list<T> t) {}

auto val = {1, 2, 3, 4}; // std::initializer_list<int>
foo({1, 2, 3, 4}); // std::initializer_list<int>

auto in generic lambda

In generic lambda auto uses the same deduction rules like for templates not for auto! This happens because, lambda is struct, so generic lambda is a template structure.

auto lambda = [](auto&& first, const auto& second, auto third) {}

is equal to

struct Lmabda {
    template <typename X, typename Y, typename Z>
    auto operator()(X&& x, const Y& y, Z z) const {

    }
};

std::forward once more

If we want to perfect forward some value in template you will write:

template <typename T>
void fun(T&& t) {
    other(std::forward<T>(t));
}

But how to do this in lambda? We know that generic lambda is a teplate, but we don't have an access to T!

auto lambda = [](auto&& t) {
    other(std::forward<decltype(t)>(t));
};   

decltype

Decltype return a type of variable, without removing references or const/ volatile qualifiers

int x = 5;
decltype(x) y; // int

const int num = 20;
decltype(num) num2 = 30; // const int

const int& ref = num;
decltype(ref) ref2 = x; // const int&

const char name[] = "Mateusz";
decltype(name) name2 = "Scott"; // const char[]

decltype(foo) fun; // void fun(int, const string&

auto pred = [](int num){ return num % 1 == 0; };
decltype(pred(20)) val; // bool

std::vector<int> vec{1};
decltype(vec.begin()) it; // std::vector<int>::iterator
decltype(vec[0]) // int&

decltype - one problem

What is wrong with this snippet of code?

void authorize() {}

template <typename C>
auto authorizeAndAccess(C& container, size_t index) {
    authorize();
    return container[index];
}

int main() {
    std::vector<int> vec{1,2,3};
    authorizeAndAccess(vec, 2) = 10;
    std::cout << vec[2] << '\n';
}
error: lvalue required as left operand of assignment authorizeAndAccess(vec, 2) = 10;

decltype - partial solution

The same result we can achieve by using decltype(auto).

void authorize() {}

template <typename C>
auto authorizeAndAccess(C& container, size_t index) -> decltype(container[index]) {
    authorize();
    return container[index];
}

int main() {
    std::vector<int> vec{1,2,3};
    authorizeAndAccess(vec, 2) = 10;
    std::cout << vec[2] << '\n';
}

decltype - when solution make another trouble

What is wrong now?

void authorize() {}

template <typename C>
decltype(auto) authorizeAndAccess(C& container, size_t index) {
    authorize();
    return container[index];
}

int main() {
    const auto res = authorizeAndAccess(std::vector<int>{5, 8, 12, 16}, 2);
    std::cout << std::boolalpha << "res: " << res << '\n';
}
cannot bind non-const lvalue reference of type std::vector<vec>& to an rvalue of type std::vector<int>
const auto res = authorizeAndAccess(std::vector<int>{5, 8, 12, 16}, 2);

decltype - final fix

void authorize() {}

template <typename C>
decltype(auto) authorizeAndAccess(C&& container, size_t index) {
    authorize();
    return std::forward<C>(container)[index];
}

int main() {
    const auto res = authorizeAndAccess(std::vector<int>{5, 8, 12, 16}, 2);
    std::cout << std::boolalpha << "res: " << res << '\n'; // will print 12
}