C++_Type_Auto - RicoJia/notes GitHub Wiki

========================================================================

Types

========================================================================

  1. %zu, %lu can be used in printf() to display size_t (note that %zu was added in C89)
    • #Include<stdio.h>
    • size_t is %lu
  2. array and ptr are different: array contains size, so works with sizeof. ptr does not.

template type deduction, There are 3 cases for template type deduction.

  1. Suppose you have 3 types of inputs:

    int i = 1; 
    const int cx = i;  
    const int& rx = i;  
    • General Rules of thumb:
      • The reference-ness in the object being passed in is ignored, during type deduction. You can choose to do T& to add that & back to all objects being passed in. -when deducing for universal references, lvalues will get special treatment
      • when deducing by-value params, const and volatile are ignored. (tricky one is const char const ptr)*
      • arrays usually decay to pointers, unless they're initialized as references.
  2. T& or T* , any reference/pointer type - this is the most regular type

    template <typename T>
    void f(T& param){};
    
    f(i);		//T will become int, so param will be int&. 
    f(cx);		//T will be const int, param will be const int&
    f(rx);		//T will be const int, param will be const int&
    f(7); 		//Invalid as we're trying to pass rvalue into f
    
    template <typename T>
    void g(const T& param){}; 
    g(i);			//now param is automatically const int, const must be forced
    
    template <typename T>
    void g(const T&& param){}; 
    g(i);			//now param is automatically const int&&, const must be forced
    
    template <typename T>
    void p(T* ptr){}		//following the same rules as the reference case
    • if T already has const, then param will have to be const
      • So const T&& is a universal reference, not an r-value reference
  3. T&&, universal reference,see below for what universal reference is.

    template <typename T>
    void f(T&& param); 
    f(i);		//T will become int&, which makes param int& thru reference collapsing
    f(cx);	//param will be const int&
    f(rx);	//param will be const int&
    f(27);	//param will be int&&
    • T&& is able to distinguish rvalue and lvalue, and param will turn out to be the corresponding reference type.
  4. T as a pass-by-value item

    template <typename T>
    void f(T param){}
    f(i);		//param will be int, easy
    f(cx);	//COUNTER_INTUITIVE: cx will be int, not const int!!
    f(rx);	//COUNTER_INTUITIVE: rx will be int, not const int!!
    
    // edge case
    const char* const ptr = "hola";			
    f(ptr);		//the ptr as an object is a const ptr, so this constness is ignored. But the pointee is of const char, that has nothing to do with the pointer itself. So that constness is preserved. param is of const char
    • We are passing in a new object param here
    • The new object param, ignore constness of the original object, because we believe we can modify it.

Decltype, Std::result_of

  1. Motivation
    • To extract type, with few surprises
    • gives the type information in compile time. typeid is in runtime.
  2. Basic Uses
    const int i = 0;    // decltype(i) will return const int.
    struct Point{
        int x, y; 
      };      //decltype(Point::x) will be int
    Point p;  //decltype(p) will be Point

    //Attention: since operator [] always returns reference,
    vec[0];   //decltype will return int&
  • So decltype can return & or &&!
  1. Good Uses
  • For declaring variables (C++14)
      Widget w; 
      const Widget& w2 = w; 
      auto w3= w2;    //w3 is const widget
      decltype(auto) w4 = w2;   //w4 is const Widget&
  • Use decltype to deduce return type of functions (and, or lambda)
    • C++11 and C++14 has different use cases and forms.
      • C++11: form is called "trailing return type"
        • use it on all template functions with "auto" return types
          • auto doesn't do type deduction here, it just tells the compiler to watch for trailing return type
          • We call it trailing return type because it has "->";
        • use it on lambdas which needs to return references
        • even work with [] on std::vector
      • In C++ 14,
        • auto in C++14 can deduce return type for the most part, so we don't need it most of the time
        • Use to to help auto to return a reference, as auto in return types will ignore reference-ness
      // General: 
      // 1. auto will ignore &, even tho c[i] should always be a reference. So when you want to return a reference, use decltype. 
      // 2. Use std::forward, if user wants lvalue/rvalue. If wants rvalue, we return rvalue reference which can be copied directly. Otherwise, you have an extra copy for building the return value
      
      // C++11
      template <typename Container>
      auto access (Container&& c, unsigned int i)
        -> decltype(std::forward<int>(c[i]))
      {
          return std::foward<int>(c[i]); 
        }
    
      // C++14
      template <typename Container>
      decltype(auto)
      access(Container&& c, unsigned int i){
          return c[i];    
        }
  • get type of a function: you need to pass in the pointer: decltype(&func)
  1. std::result_of
  • Used to get the type of the return type of a function (deprecated in C++17)
    template <typename F, typename...Ts>
    inline 
    std::future<typename std::result_of<F(Ts...)>::type>
    trueAsync(F&&f, Ts&&...params){
      return std::async(std::launch::async, std::forward<T>(f), std::forward<Ts>(params)...);
    }
  1. EXCEPTIONS
    • if an lvalue expression other than a name has type T, decltype will return T&
    int i = 3;
    decltype(i);    //return int
    decltype ((i));     //return int&
    • decltype() value categories:
      • xvalue, yields T&&
      • prvalue, yields T
      • lvalue, yields T&
        • so decltype(1+2) is int, not int&&
        • decltype(std::declval<T>())>::value) even though std::declval<T>() returns an rvalue reference, it's a prvalue ehre

typeid

  1. Basic Uses: comparing types

    #include <typeinfo>
    cout<<std::typeid(var).name()<<std::endl; 
    
    class Widget; 
    Widget w; 
    cout<<std::typeinfo(w).name()<<endl;    
    
    // comapring types. 
    struct Base{}; 
    struct Derived : Base {};
    cout<<(typeid(Base) == typeid(Derived));
    • PKd menas "Pointer to Konst deouble"
    • PK6Widget means Pointer to Konst Widget, (widget has 6 chars)
  2. Print type inside templated function

  template <typename T>
  void f(const T& param){
      cout<<typeid(T).name(); 
      cout << typeid(param).name(); 
    }

See the type of a variable

  • IDE: good for some basic types, has a compiler running at the background
  • Compiler diagnostics: will give you more useful info
      template <typename T>
      class Test;     //Anything calls this won't compile because there's no template definition
      Test<decltype(x)> xType;    //So during compile time, we can see the type of this. 
  • Use typeid to see type in runtime.
  • But sometimes, types are still not accurate enough

Type Alias

  1. type alias is just a way to alias a type! aTutorial. Will be useful for backward compatibility!
    struct Type{
      template <typename T>
      using type = T; 
    };  
    Type::type<int> i = 5;    // "fancy" way to initiate an int. 
    • it cannot be used to store a type, since a type is not an object

By pass an rvalue ref to a temporary object of T

  1. std::declval<T>(), returns an rvalue ref to a temporary T object, so no need to call default ctor, which T may not have
    #include <utility>
    #include <iostream>
     
    struct Default { int foo() const { return 1; } };
     
    struct NonDefault
    {
        NonDefault(const NonDefault&) { }
        int foo() const { return 1; }
    };
     
    int main()
    {
        decltype(Default().foo()) n1 = 1;                   // type of n1 is int
    //  decltype(NonDefault().foo()) n2 = n1;               // error: no default constructor
        decltype(std::declval<NonDefault>().foo()) n2 = n1; // type of n2 is int
    }

========================================================================

Auto

========================================================================

  1. Motivation

    • Came about in C++11, type deduction used to happen in compiler phase, but with auto, it happens in run time.
      • means return type will be evaluated in runtime, by deducing from its initializer.
          auto a;		//compiler error, needs to be initialized
          auto a = 0; 
  2. Short summary:

    • Auto is essentially the same as template param T below:
        template <typename T>
        void foo(T param);      //T is a copy itself, so you need to add const, or & to make it a reference! but constness is preserved. 
      • Assume you have below vars
      int i = 1; 
      const int j = i;  
      const int& k = i;  
      • Case 1 auto + non-universal reference
        template <typename T>
        void foo (T& param);      //equivalent, so const-ness is preserved
        auto& a = i; //int& 
        auto& a = j; //const int& 
        auto& a = k; //const int & 
        auto& a = 7;  //invalid for rvalue
        
        auto&& a = 7;     //valid, a is an lvalue object, of rvalue type
        func(std::forward<int>(a));     // You need to forward a to pass it as a real rvalue
        
        // The misconceptions: 
        const int&& a = 7;    //actually const int&?
        const auto&& a = 7;   // same as above
      • Case 2: auto + universal reference
        auto&& a = 7;   //rvalue
        auto&& a = i;   //lvalue
      • Case 3, auto itself creating a copy, like template, constness and volatile are ignored
        auto a = j;   //ignore const 
        auto a = k    //ignore ref.
        auto func(int& t){
          return t;   // actually, auto is just like T in template, which will strip off &
          }
  3. Exception The only difference from template is in initializer list (C++11 & 14): auto will be deduced into std::initializer_list<int>, but template won't and gives you an error

    auto a = {1,2,3};   //a is deduced into std::initializer_list
    bar(a);   //valid
    bar ({1,2,3});    // Template funcs cannot deduce {} into std::initializer_list
    bar(std::initializer_list<int>{1,2,3});   //valid
    1. but in lambda, auto is the same as template - it can't deduce {} into std::initializer_list
      // auto param
      auto func = [](const auto& newVal){};
      // func({1,2,3});      // can't deduce to {1,2,3}
      func(std::initializer_list<int>{1,2,3});
    2. in C++ 14, auto is the same as template function in return type deduction
      auto createList = [](){
        // return {1,2,3};    // can't deduce to std::initializer_list<int>
        return std::initializer_list<int>{1,2,3};
      };
  4. In C++11, you can use auto for deducing return type of a function, but must be used with decltype:

    template <typename A>
    auto do_something(const A& a)
      -> decltype(a)
    {
        return A;
    }
    • Caution: You should always make sure auto is deduced before the function is used
      • auto deduces during compile time.
      • But never ever define auto function in another file
      • In class members, if another member depends on the type deduced by an auto function, there'll be an error, as data members are declared before auto gets deduced.
  5. auto&& universal reference param

    auto foo = [](auto&& func, auto&& params){};
  6. Good uses of auto

    • iterator auto i = c.begin()
    • auto can be used to declare a lambda expression as well. It's faster, less fuss
      • std::function can store any "callable objects", (class with overloaded(), lambda,etc.), aims to replace function pointer.
      • std::function is slower, and takes up more space, because it the object itself takes up fixed amount of memory on stack. If memory is not enough, it will go to heap
    • avoid small & hidden errors from explicitly writing things out:
      • std::vector::size actually returns std::vector::size_type, which is 64 bits on windows 64 bits, while unsigned int is 32 bits.
        std::vector<int> v; 
        unsigned sz = v.size(); 
  7. you can have auto member variables, but they've to be static const:

    struct Timer {
        static const auto start = 0;
    };
  8. Cautions

    • reference needs to be auto&, even if it gets a reference
       auto& n = fun();	//even if fun() returns int reference, without&, n will be int. 
  • auto a; //needs initialization
  • "Invisible Proxy Classes": do not do auto var = (std::vector<bool>{true, false})[5]; , explicitly force the type instead, see below
    • Proxy classes mean to emulate the behaviour of a simple thing, like smart pointers.
    • Visible ones can be used directly
    • while invisible ones, you don't even know that they exist, but you use it as an intermediate product. That's from implicit conversion
        void func(bool); 
        auto var = (std::vector<bool>{true, false})[5]; 
        func(var);    //Compilation error!
      • usually, operator [] in std::vector will return T&

      • One exception is std::vector will return std::vector::reference, which is an invisible proxy

        • It can implicitly convert to bool
        • It exists because: std::vector stores a bool in a bit, but C++ forbids reference & to a single bit. So, this is emulating a full 1 byte bool.
        • std::vector::reference internally is a pointer to the bit. Since we have a temporary vector, the pointer will become dangling, after this line.
      • invisible proxy classes usually are not compatible with auto.

  • Explicitly forcing a type:
    bool var = (std::vector<bool>{true,false})[5];  // This is not recommended as it's not crystal clear
    auto var = static_cast<bool>(std::vector<bool>{true,false});    //This is recommended

========================================================================

Casting

======================================================================== casting means to force a data type convert to another.

  • c-style cast: will allow some incompatible casting during runtime but ok in compilation, e.g,

    char c = 10;
    int* p = &c;        //okay in compilation, but char is 1 byte, int is 4 bytes, during runtime more bytes will be written and causes error 
    (double)c; 
  • static_cast:

    • double to int, unsigned int cast. see code

    • simplest type of casting, happens during compilation, it checks if types are compatible:

       int* p = static_cast<int*>(&c); 
    • Static Cast also allows converting from & to classes

       class Int{
       	public: 
       		Int (int i): i(i){};	//This is a "coversion ctor. "
       		operator std::string(){return std::to_string(i); }		//conversion operator, This can be used TO convert to string. 
       		int i; 
       	}
       int main(){
       		Int obj(3);		 
       		std::string str = obj;		//calls conversion operator
       		str = static_cast<std::string>(obj)		//conversion operator, from obj to string
       		obj = static_cast<Int>(100);	//conversion constructor, from int to Int. 
       	}
    • Static Cast can be used in polymorphism as well, but only from derived to base

      • derived->base as derived always has base.
         class MyDerived : public MyBase{}; 
         MyBase* = static_cast<MyBase*> ptr;	//Simplest Usage
        
         class MyDerived2 : protected MyBase{}; 
         MyBase* = static_cast<MyBase*> Derived2_ptr;	//protected, private child class are not accessible!
        • If static cast is used on inheritance, have public instead of private inheritance!!
        • DO NOT USE IT ON base->derived casting, as this will be allowed, but might cause run-time errors since the object is incomplete.
    • Static cast from & to void*

       int main(){
       	int a = 5; 
       	const int* ptr = &a; 
       	int* ptr_2 = const_cast<int*> (myInt); //without const_cast, there'll be problems. 
       	}
    • static_pointer_cast for converting smart pointers

        std::unique_ptr<int> u_ptr (new int{1}); 
        *(std::static_pointer_cast<int>(u_ptr));        //Note that we're using std::static_pointer_cast
      
  • Dynamic_cast **This is usually used when but base -> child (downcasting), but it can be used for child -> base class as well. It performance compatibility check in runtime. **You can use it:

    1. derived -> base
    MyBase* = dynamic_cast<MyChild*> (ptr);       //can also use static_cast
    1. base -> derived
    //In an array of ptr
    MyChild* = dynamic_cast<MyChild*> (ptr);   //this will succeed if ptr points to a child class object, or it will return **null** if ptr points to a base class object.
    if(!ptr){...}       //**use !ptr here, I am not sure what type this is?**  
    1. base -> derived reference
    #include <exception>
    try{
        MyChild &child = dynamic_cast<MyChild&>(*base); 
    }
    catch(std::exception &e){
    cout<<e.what(); }
    • Cautions:
      • can only be used for base -> child when you ACTUALLY have a child class obj, else you will get a null.
  • const cast It's used for letting a const value be modified. Uses:

    • pass const data to function doesn't receive const
       int a = 5; 
       const int* ptr = &a; 
       int* ptr_2 = const_cast<int*> (myInt); //without const_cast, there'll be problems. 
      but **const cast doesn't allow modify a value initially declared as const, as it's in read-only memory **
       int a = 5; 
       const int* ptr = &a; 
       int* ptr_2 = const_cast<int*>(ptr); 
       *ptr_2 = 5;		//NOT ALLOWED
      
    • Let const member function modify a member variable
       class Foo{
       		public: 
       			int i; 
       			fun(){(const_cast<Foo*>(this)) -> i = 5; }
       	}
       const Foo f; 
       f.fun(); 
    • Also, const_cast doesn't allow casting different types
       int a1 = 5; 
       const int* b1 = &a1; 
       char* c1 = const_cast<int*>(b1); 
  • reinterpret cast

    • Motivation: simply treats one pointer as another, with no checks. So it's dangerous to use. Static_cast does strict type conversion checking, and after conversion, the object is properly converted. Reinterpreted_cast doesn't do that

      • Typically only used for converting ptrs of different integral types
         int a1 = 40; 
         const char* b1 = static_cast<char*>(&a1);		//Don't work as there's no explicitly way to convert them
         const char* b1 = reinterpret_cast<char*>(&a1);	//but char is an integral type, you can actually do this.
    • Demo only, do not use it:

       class Foo{
       		void fooFunc(); 
       	}
       class Bar{
       		void barFunc(); 
       	}
       int main(){
       		Foo* foo_ptr = new(Foo);
       		Bar* bar_ptr = reinterpret_cast<Bar*>(foo_ptr); 
       		bar_ptr -> barFunc();		//In this case it compiles, because compilers secretely changes the function signature to Bar::barFunc(Bar* this), then uses this pointer to access the member variables. 
       	}
    • Cautions:

      • after type_casting, it becomes "non-portable", i.e, you might on other achitectures (ARM, X86), you may run into trouble with the results.

Type Misc

  • typeid vs decltype

    • typeid(x).name() gives name of the type, declared with auto: i for integer, d for double
    • decltype determines type in compile time, so it might gives base class type, while typeid gives derived class type.
  • create temporary for auto (c++17)

     auto = std::string("Hello world");	// no temprorary is created.
  • auto , decltype, and initializer_list

     auto k = {"str", "lol"};	// copy initialization is allowed with auto. BUT THIS REALLY IS A SPECIAL CASE, AS THE COMMITTEE THINKS auto should still work in this case. 
     decltype({"str", "lol"}i);	// Anything other than auto cannot use {} directly, as {} is just an expression, which doesn't have a type
    
  • Array

    • From C, array always decays down to ptrs. Samething happens to template.
    • However, we get to preserve the array (i.e, address and size) if we pass it in as a reference
      const char name[] = "hola"; 
      // Case 1: array-to-ptr decay
      template <typename T>
      void f(T param); 
      f(name);		//param will be ptr. 
      
      // Case 2: preserving the array
      template <typename T>
      void g(T& param){}
      g(name)		//param will be a full array!
      - Use: useful for getting the size of array during compile time, **combined with constexpr**. which can be used to create other arrays, 
      ```cpp
      template <typename T, std::size_t N>
      constexpr std::size_t getArraySize(T(&)[N] param) noexcept{	//constexpr is KEY. noexcept is for generating better code?
          return N; 
        }
      
      //Use
      int arr[] = {1,2,3,4}; 
      int arr2[getArraySize(arr)];		//getting array size in compile time
      ```
      
  • Functions: similar to array,

    • functions will decay to function pointer
    • ref to function will still be the function!
  • "Incomplete Type": a type that has been declared, but lacks info to determine its size

    • structure type declared but no definition for its data members
    • union type whose members have not been defined
    • array with no defined size.
    • if you have incomplete type error, maybe you have forward declared the class, but you shouldn't (cuz it exists somewhere already)
⚠️ **GitHub.com Fallback** ⚠️