C++_Design - RicoJia/notes GitHub Wiki

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

Idioms

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

Copy and Swap idiom (TODO)

Erase Remove Idiom

  • remove a char using erase-remove idiom:
    str.remove(std::erase(str.begin(), str.end(), 'a'), str. end())

pimpl idioms Example and explanation

  • Motivation

    • If we have a lot of private data members in h file, every time we change them, we have to compile all the dependent files.
      • Don't we compile all together?
    • By putting these data members in a class (Pimp, pointer to implementation), in cpp file, we have shorter compilation time
      • private data hiding technique called pimpl idiom.
    • The class is accessed thru an "opaque pointer" (pointer you cannot see thru).
  • Example and Explanation

    • .hpp
      class PRM{
      public:
          PRM();
      
          // Don't define the below, this is key!
          // we generally don't need dtor, but because of unique_ptr's quirk, we need to define it, hence defining all big 5. 
          PRM(const PRM& prm); 
          PRM& operator = (const PRM& prm); 
          PRM(PRM&&);
          PRM&& operator= (PRM&&);   // This is rvalue reference 
          ~PRM();     
      
      private:
          struct impl;    //Forward declaration. 
          std::unique_ptr<impl> pimpl;
      };
    • Eg
      struct PRM::impl{   // This is a member of impl
        // We technically need all big 5, since ~impl() is  a must. But, we can get the code compile without them. 
        // However, if we don't have move, then we don't get performance boost. 
        // So still, it's better to have the big 5. 
        double map_x_max = 10;
        double map_y_max = 10;
        impl(double x, double y): map_x_max(x), map_y_max(y)
        {}
      };
      
      // Now let's define the special member functions that were not defined
      PRM::PRM(double x, double y):pimpl(new impl(x,y))  //initialize impl. you can also use std::make_unique
      {
          std::cout<<pimpl->map_x_max;//
          std::cout<<"hello";
      }
      
      PRM::PRM (const PRM& prm){
          *pimpl = *prm.pimpl;    // Making use of the default copy = from impl
          <!-- pimpl.reset(std::make_unique<impl>(*(prm.pimpl)));  -->    // This is wrong - not invoking impl ctor correctly
        }
      PRM::PRM (const PRM& prm){
          pimpl.reset(prm.pimpl)
        }
      
      // Must be here
      PRM::PRM(PRM&& prm) = default;      // Below is not needed, because the default will do move on unique_ptr, and unique_ptr will call the default move from Impl
      <!-- PRM::PRM(PRM&& prm){ -->
      <!--     pimpl=std::move(prm.pimpl) -->
      <!--   } -->
      // Must be here
      PRM& PRM::operator=(PRM && pimpl) = default;    // For the same reason above, below is not needed 
      <!-- PRM& PRM::operator=(PRM && pimpl){ -->
      <!--     pimpl = std::move(prm.pimpl)   -->
      <!--   }    //move assignment -->
      // Must be here
      PRM::~PRM() = default;    // IMPORTANT: This way std::unique_ptr in PRM can see the full definition of Impl
      //Needs to be defined after impl definition. The reason is unique_ptr's destructor looks for the full definition of impl, in comparison, raw pointer doesn't.
      // if define destructor in .h, it will be inline destructor, then it can't see the full definition of impl.
      • Notes:
        • we declared dtor in hpp only, because:
          • Dtor of Foo will call the destructructor of std::unique_ptr, which calls delete on the object
          • But before that, std::unique_ptr will first check if the pointee is an incomplete type (declared but not defined)
            • using static_assert
          • So to fix that, we need to let the dtor at least see the full definition of Foo::Impl
          • So just put the dtor after Foo::Impl
        • We declared the move ctor, = in hpp only, and were required to define them after the struct definition
          • Because move will destruct the object upon exception in move constructio
          • So move will not call dtor, instead it will synthesize a similar code
          • So for similar reasons, we should define them after the defintion
        • So we define our copy ctor, = as well
        • We shouldn't use std::shared_ptr, but if we did, none of the above will apply
          • The difference is unique_ptr has destructor as its template type, for generating smaller code. That requires a complete type
          • std::Shared_ptr does not demand complete type, and it's happy with not seeing the complete type, and having implicitly generated code only.
  • Pros and Cons

    • Pro 1 Binary compatibility: binary interface is independent of the private fields, so changes to the implementation will not break dependent code
    • Pro 2 shorter compilation time
    • Cons 1 more complex maintenance
    • Cons 2 Hidden implementation cannot be inherited?? //TODO

RAII (Resource Acquisition is Initialization, RA double I)

  1. Resource allocation is done during object creation, especially initialization. Resource is released during destruction. The essense of RAII is to give ownership of a heap-allocated object to a stack-allocated one.
    • Resource: allocated memory, locked mutex, thread execution, open file
  2. Resource allocation must succeed for resource allocation to succeed. If and only if the object is alive, the resource is being held.
  3. Stack-Unwinding: all destructors of objects on the stack will be called, at the end of the enclosing scope, in reverse order, whether or not there are exceptions.
  4. local variables will be destructed only if constructed fully, at the end of the function scope. Therefore, RAII makes memory management easier.
  5. Example
    std::mutex m; 
    void bad(){
    	m.lock();       //acquire the lock without being in an RAII object
    	do_something();     // If throws exception, lock will never be released
    	if(!everything_good())
    		return          // Lock will never be released during in early exit 
    }
    
    void good(){
    	std::lock_guard<std::mutex> lk(m);  // acquire lock in an RAII object, which will be destructed regardless of exceptions.  
    	do_something();                     //Even if there's an exception, lock can still be released.                           
    	if(!everything_good()) return       // lock can be released by RAII object here
    }

Factory Method

https://sourcemaking.com/design_patterns/factory_method/cpp/2 https://sourcemaking.com/design_patterns/factory_method/cpp/1

  1. Motivation: When you design a framework, you usually have the entire control flow. But in your framework, you might have instances of different subclasses of a baseclass (e.g, in a car's framework, you might have different subclasses of the "engine" class, like gasoline and disel. ), like a factory makes products with the same public interface, but different internal workings.
  2. Core mechanism:
  • Static base class member function to manufacture subclass functions on demand.
    • sometimes, you may not need static function. Instead, you make an instance of the child class, and have an overloaded function to return a pointer to you
  • Polymorphism for making different sub-class objects In your main code, you still need to have
    BaseClass* engine = new Diesel(); 

To create a new instance of an engine, which is directly related to the "polymorphism". To make your design more elegant, you can make a "factory function", instead of using new.

class Engine{
    public: 
    // factory method.  why is this static? 
    static Engine* create_engine(int choice); 
};

Engine* Engine::createEngine(int choice){
    if (choice == 1) return new Diesel;
    else return new Gasoline; 
}

int main(){
    Engine* engine = Engine::createEngine(1);   // create a diesel engine. 
}     

Singleton

  1. Motivation: makes sure only one instance of the object can be created (like cache, etc.).
  2. Mechanism: static keyword (the object persists.) + private constructor(so nobody can create it outside the class, like using new)
  3. Example:
    class Omnid_Group_Planner{
        public: 
            static Omnid_Group_Planner* getInstance(){  
            //static member functions can only access static member variables. 
                if(!object_ptr){
                    data = 1; 
                    object_ptr = new Omnid_Group_Planner(); 
                }
                return object_ptr; 
            }
    
            
            int getData(){return data; }
            void setData(int d){data = data; }
            
        private: 
          static Omnid_Group_Planner* object_ptr;
          ros::NodeHandle nh; 
          int data; 
    
          ~Omnid_Group_Planner(){	//prevent unwanted freeing 
             delete object_ptr;//TRAP: this will cause an infinite loop, as delete will call this destructor again.  
            object_ptr = NULL; 
            }
          Omnid_Group_Planner(const Omnid_Group_Planner&); // prevent copying it. C++03. In C++11, use delete
          Omnid_Group_Planner& operator=(const Singleton&);	//prevent assigning it.  C++03. In C++11, use delete
          Omnid_Group_Planner(){data = 0; }
    }
    
    //In main.cpp
    int main(){
        auto ogp_ptr = Omnid_Group_Planner -> getInstance(); 
        int data = ogp_ptr -> getData();  
    }
  4. Best Practice

Strategy

  1. Motivation: you have 5 characters in the game, but each character has either an attack ability, or a jump ability, or both. Also, each person's attack/jump ability may or may not be different than others.
  • In this case, inheritance of the same interface methods does not work well, because one character might not need that method.
  • If you use abstract functions to make an interface, it does not work well either, because two characters may have the same ability. So you will end up implementing the same thing. Therefore, you want to use inheritance and composition at the same time. **Inheritance is to set a base class that may have those modules, composition is to include each different abilities using polymorphism. If one character does not have one, simply set the base class's ability pointer to null **
  1. Core mechanism: 1.Inheritance for making the base classes for characters, modules 2.**Polymorphism for passing in different modules and storing them in the same place in each character. **

  2. example

    // mob.cpp
    **//Be careful with forward declaration since it's circular reference**
        #include "ability.cpp"
        class Ability; 
        class Mob{
            public: 
                Mob(): ability_ptr(nullptr){}
                Void setAbility(A)
                int blood; 
            private:
    **//This is polymorphism that allows u to use the right ability**          
                Ability* ability_ptr;   
        }
        
    //ability.cpp
    **//Be careful with forward declaration since it's circular reference**
        #include "mob.cpp"
        class Mob; 
        class Ability{
            public: 
                virtual void performAbility(Mob* self, Mob*victim); 
        } 
        
        class Attack::Ability{
            public:
                virtual void performAbility(Mob* self, Mob*victim) {--victim->blood;}
        }
        
        
        class Jump::Ability{
            public:
                virtual void performAbility(Mob* self, Mob*victim) {++self->blood;}
        }
    //main.cpp
        #include "mob.cpp"
        #include "ability.cpp"
        
        Mob angry_pen(); 
        Mob happy_pen(); 
        angry_pen.setAbility(new Jump());   //use polymorphism to set ability. 
        happy_pen.setAbility(new Attack()); 
    

Observer

  1. Motivation: when there is one "broadcaster" that needs to update all its subscribers (observers), This is the way to go.
  • The broadcaster has an array of observer pointers to store all observers. Pointers will allow polymorphism.
  • Note that this is a bi-directional structure. observers calls the broadcaster's functions to add and remove itself from the vector. Broadcaster will call an observers's function about updates.
  1. Core Mechanisms: referencing/pointers for notifying subscribers with updates & broadcasters for attachment, detachment

  2. Example

class Broadcaster{
public: 
    Broadcaster(){}
    attach(const Subscriber* ptr){observer_arr.push_back(ptr);}   // std::weak_ptr is better 
    detach(const Subscriber* ptr){
        auto itr = observer_arr(std::remove(observer_arr.begin(), observer_arr.end(), ptr), observer_arr.end());
    }
    updateAllObservers(int i){
        for (auto& ptr : observer_arr){
            ptr -> update(i);   //calling the observer's function to update.  
        }
    } 
private: 
    std::vector<Subscriber> observer_arr; 
}

class Subscriber{
public: 
    Subscriber(const Broadcaster& b){b_ = b; }
    bool update(int data){data_ = data;}
    virutal void detachMe(){b.detach(this); }
    virtual void attachMe(){b.attach(this);}
private: 
    int data_; 
    Broadcaster& b_; 
}

class LolSubscriber: Subscriber{
public: 
    LolSubscriber(const Broadcaster& b): Subscriber(b){Subscriber::attachMe();}
    void detachMe(){cout<<"lol"; Subscriber::detachMe(); }
}   

//main.cpp
int main(){
    Broadcaster b(); 
    LolSubscriber* s1 = new LolSubscriber (&b); //**You don't have to declare the subscriber as a pointer, but you need to pass a pointer into the Subscriber**
    b.update(1010101);      //update broadcaster. 
    s1 -> detachMe();   //**you can place this in its own destructor as well**
    delete s1; 
}
  • A big improvement is using std::weak_ptr. Because:
    • You have no interest in managing each observer's lifetime
    • But you're interested in if they're still alive

Builder

  1. Motivation: so right now you want to produce a product (class Product), the product has many different features. Each feature is built by a builder(class Builder), but we need different types of builders, so there are (class ConcreteBuilder) under it. It's suitable for one base class with many different implementations of the same method

  2. Core Mechanisms: Polymorphism for having different builders, but same overall procedure.

  3. Example

    class Product{
    public: 
        Product(){}
    private: 
        std::vector<string> parts_;      //or you can have parts of different types. 
    }
    
    class Builder{      //an interface
    public: 
        Builder(){}
    private: 
        virtual void buildPartA() const = 0;    //need const because you don't want to modify anything in the class.   
        virtual void buildPartB() const = 0; 
        virtual void buildPartC() const = 0; 
    }   
    
    // You can have treehouse builder as well ;)
    class IglooBuilder:Builder{
    public:  
        ConcreteBuilder():Builder(){}
        ~ConcreteBuilder(){delete product_;}
        void buildPartA () override {product_ -> parts_.push_back("partA");}  
        void buildPartB () override {product_ -> parts_.push_back("partB");}  
        void buildPartC () override {product_ -> parts_.push_back("partC");}  

//override will make the compiler check if the base class has the exact signature. // Also, we expect the builder to build a full product a certain type. Now it's type A // The reason we have the base class Builder is the same sort of "workflow" private: void startNewProduct(){product_ = new Product(); } Product* getProduct(){Product* ret = product_; startNewProduct(); return ret;}
Product* product_; // Because the type of product will be different, we implement the getProduct function separately. }

// Some people like to implement a Director class, but that's really optional

//int main
int main(){
    Builder* builder = new IglooBuilder(); 
    builder -> buildPartA(); 
    builder -> buildPartB(); 
    builder -> buildPartC();
    Product* product = builder -> getProduct();
    delete product; 
    delete builder; 
}   
``` 

Adapter

  1. Motivation: it's like you have an Arduino serial, and one USB. You make an SerialToUSB adapter, such that serial(adaptee). and USB(target) are standalone, but we have information flow: serial -> adapter -> USB, by having function calls USB -> adapter -> Serial

  2. Polymorphism for the adapter tricking the target.

  3. Implementation

        #include <string.h>     //memcpy
        class Serial_Adaptee{
        Public: 
            Serial_Adaptee(uint_8 data) : data_(data){} 
            uint_8 data_;     
        }
        
        //information source for USB_Target
        class USB_Source{
        public: 
            USB_Source(uint_16 data) : data_(data){}
            uint_16 emitData(){return data_;}
        private: 
            uint_16 data_; 
        }
    
        class USB_Target{
        public: 
            USB_Target(USB_Source* src) : src_(src), data_(0){}
            void transmitData(){data_ = src->emitData();}
        private: 
            const USB_Source* src_;  
            uint_16 data_;
        }
        
    //Adapter will "fool" the target by pretending to be its information source. 
        class Adapter : USB_Source{
        public: 
            Adapter(Serial_Adaptee& serial) : USB_Source(0), serial_(serial){}
            uint_16 emitData(){
                uint8 data = serial_.emitData(); 
                memcpy(data_, data, 1);     
                return data_;
            }
        private: 
            Serial_Adaptee& serial_; 
        } 
        
        
        int main(){
            Serial_Adaptee serial(100);
            Adapter adapter(serial);  
            USB_Target usb_target(&adapter);
            usb_target.transmit_data();  
        }

State TODO

Define your own type

#ifdef COSTTYPE
#define COSTTYPE unsigned char
#endif

Non-Virtual-Interface (NVI) (aka Template Method Pattern, template here means an "skeleton" structure)

  • Widely used in STL!!
    class shape{
    public: 
        int area(){
            if(stateIsValid()){throw std::logic_error("Not valid shape");}  //you have the same big workflow, but just one func is different
            return this -> calcArea(); 
        }
    private: 
        virtual calcArea();     // prefer to have virtual funcs private. Make it protected if child class needs to invoke the same thing. 
    }
    • Guidelines:
      • use private virtual functions.

Design Pattern

  • Some applications might have a while loop in the loop in run in, seems like it's blocking the thread
    • a good pattern is: having start() separate from constructor, So you this function may be put on a different thread, while you have the object sitting in the main thread
    • close() separate from destructor (close the sockets, but not destruct the object yet). so you the main thread can terminate the thread, while the thread is still joinable (else, the object will be destructed, and the thread will not be joinable.)
     class webserver{
     	...
     	void start(){app_.port(port_).multithreaded().run(); }
     	//Stop the webserver
     	void cleanup(){app_.stop(); }
     	}
     int main(){
     	util::Webserver ws();
     	// So you can put start on a separate thread, while you still have control over it. 
     	std::thread ws_th(&util::Webserver::start, &ws); 
     	//DO STUFF
     	// You also can terminate the thread. And the thread will be joinable
     	ws.close(); 
     	ws_th.join();
     }

Misc

  • Semaphore - semaphore allows n number of threads to enter, while mutex can only allow one thread to enter.

Small Design Choices

  1. same interface functions like tf2::toMsg(Intype i) in ROS
    • you might have function overloading for functions with the same arguments. Therefore, they do tf2::toMsg(Intype i Outtype o);
  2. Callback
    • widely used in server-side programming
    • can be used to store any callable object: something that can be called as a function.
      • A C fucntion pointer
      • A Lambda Expression
      • A functor (an object with overloaded())
  3. managing thread & object: easy to check and terminate!
using obj_thread = std::pair<obj*, (std::thread)*>
obj_thread* getObjThread{
    obj* obj_ptr = new obj();   //since we're using new, the object won't be destroyed by stack
   return new obj_thread(obj_ptr, new std::thread(obj::func, obj_ptr)); 
  }
int main(){
    ...
  threads.emplace_back(getObjThread()); 

  for(auto i = std::begin(threads); i != std::end(threads); ++i){
      if (((*i) -> first) -> stillValid()) do_stuff(); 
      else{
          (*i) -> second -> join(); 
          threads_.erase(i++); 
        }
    } 
  }
  1. Return unique_ptr from a function
  using connection_ptr = std::unique_ptr<some_connection>; 
  connection_ptr makeConnection(){
      return std::make_unique<some_connection>();   
      // C++14 introduces make_unique; 
      // all make_unique is to 1. call new operator with args 2. makes exception-safety (returns null insteadof throwing exceptions)
      //the caller will move_construct from the returned unique_ptr. Therefore the object in this scope will be extended to the caller. 
      //Technically, unique_ptr cannot be copied. But here, since we're returning an rvalue of unique_ptr, "copy elision" allows the caller to move construct it. 
    }
  • Note: rvalue is required if we're trying to create a std::pair, std::tuple with unique_ptrs
    • If you construct unique_ptr right on the fly, copy elision allows you to do it
    • If you already constructed unique_ptr, then you need to std::move it.
        vector<unique_ptr<int>> songs;
        songs.push_back(make_unique<int>(1));     //copy-elision
        auto song = make_unique<int>(2);  
      //   songs.push_back(song);     //Error: Now you have to std::move
        songs.push_back(std::move(song));     //Now you have to std::move
  1. Handle Design Pattern
  class some_connection{
      void Handle(){
          while (1){
              ...
              if (!some_flag_) break; 
            }
        }
    }; 

  using connection_ptr = std::unique_ptr<some_connection>; 
  using connection_thread_ptr = std::unique_ptr<connection_thread>; 
  Factory(){
    connection_ptr connection = std::make_unique<some_connection>(...); 
    return std::make_unique<connection_thread>(std::move(connection), std::thread(&some_connection::Handle, connection.get()))
  }
  1. Pretty common to use a queue to transfer data.

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

Universal Reference Overloading

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

  1. Suppose you want to log employee's information, either from their name (could be rvalue reference or lvalue reference), or their ID. Their ID could be int, or short.

    • Therefore we are using template for the universal reference as well.
  2. functions with universal reference shouldn't be overloaded, because if you do,

    1. once you have small things like promotion, the universal ref function is called, because that can yield a match
      • Function called sequence: regular func with exact match > templated (including universal function) func yielding an exact match > funcs needing a promotion, or needing a const qualifier, etc.
      • You can have perfect forwarding ctors, but they are not recommended
          #include <iostream>
          using namespace std; 
        
          class obj{
              public: 
            template <typename T>
            explicit obj(T&&m) {
              cout<<"lol"<<endl;      
            }   // explicit bans implicit type conversion 
            
            explicit obj(int m) {
              cout<<"int"<<endl;      
            }  
            
            obj(){}
            // !! With template ctor, copy and move ctor can still be synthesized
            // So obj(const T&); will be the signature of the default copy ctor
            // !! But default ctor will not be synthesized 
          }; 
            
          int main(){
            obj OB; 
            obj OB2(OB); 
            obj OB3(1); // see "int"
            obj OB4((char)1); // see "lol". Because it's very easy to have conversion, overloading on universal reference is not recommended
          }
    2. Hijack base-class copy ctor, when passing derived class obj in.
      class Foo : public obj{
          Foo(const Foo& r) : obj(r){}        //same as below
          Foo(Foo&& r) : obj(r){}   // will call the perfect forwarding ctor, instead of the default ctor
      }
  3. Solution to overloading:

    1. Use tag dispatch, "dispatching the right work". Useful when you have the same function for different types. Without this, type mismatches will be found during compile time and cause errors.
      auto f_impl(std::true_type) { return true; }
      auto f_impl(std::false_type) { return std::string("No"); }
      
      template <class T>
      auto f(const T& t) {
          return f_impl(std::is_integral<T>());
      }
    2. SFINAE (Substitution Failure In Not An Error) to constrain universal references
      • Similar to tag dispatch, we use std::enable_if, see typedef_oop_temp.md for this
          class Person{
          public:
            // SFINAE ctor
            template <typename T, 
                      typename = typename std::enable_if<condition, T>::type>
            explicit Person(T&&); 
          }
        • use std::decay to remove reference and cv-qualifiers typename = typename std::enable_if<std::is_same<Person, std::decay<T>::type>::value, T>
        • Universal reference + perfect forwarding is used in this ctor
        • Drawbacks:
          • some objs can't be perfect-forwarded
          • If type-mismatches happen during perfect-forwarding, error msg can be hard to comprehend
              Person p(u"Penguin");     //u "something" is char16_t, which cannot be converted to std::string implicitly. 
              // So u"Penguin" will be perfectly forwarded to Person(const std::string&) due to 
              // std::enable_if. 
          • Solution is to add static_assert with std::is_constructible
              class Person{
                public:
                  // SFINAE ctor
                  template <typename T, 
                            typename = typename std::enable_if<condition, T>::type>
                  explicit Person(T&&){
                      static_assert(std::is_constructible<std::string, T>::value, "Error: type T cannot construct std::string"); 
                    }
                }
            

Small Paradigms

  1. function chaining paradigm:

    class Foo{
      Foo& do_thing_1(){
        //...
        return *this;
      }
    
      Foo& do_thing_2(){
        //...
        return *this;
      }
    };
    • advantages: cout<<a<<endl, << returns a cout object, so it's chaining.
    • disadvantage: make sure the object doesn't get destroyed during chaining.
  2. Python's Enumerate in C++, 涉及知识点

    #include <tuple>
    template <typename T,
              typename TIter = decltype(std::begin(std::declval<T>())),
              typename = decltype(std::end(std::declval<T>()))
              >
    constexpr auto enumerate(T&& iterable){
        struct iterator{
            size_t i;
            TIter iter;
            bool operator != (const iterator& other){return other.iter != iter; }
            void operator ++(){
                ++i; ++iter;
            }
    
            auto operator *(){
                return std::tie(i, *iter);
            }
    
        };
    
        struct iterable_wrapper{
            T iterable;
            auto begin(){return iterator{0, iterable.begin()}; }
            auto end(){return iterator{0, iterable.end()}; }
    
        };
    
        return iterable_wrapper{std::forward<T>(iterable)};
    }
    
    // Use: 
    for (const auto&[index, item]: enumerate(vec)){}
    1. for-range loop: need a proper iterator

      for (auto a: vec) => 
      for (_begin = vec.begin(); _begin != vec.end(); _beg++) {
          a = *_begin
      }
    2. enumerate should return a wrapper with begin(), end(), and those create an iterator object.

      • Cuz iterator (not singleton) cannot return an object of itself.
    3. std::forward: to allow for rvalue and lvalue refs

    4. For the iterator:

      1. Use SFINAE to tell compiler in compile time that the container does have a begin()
        • so we put TIter in template header
      2. And TIter depends on type T, so it needs typename
      3. decltype: used to return the type of something.
        • if there's &&, or with const, will be returned as well
      4. std::begin(const T& ) (c++11) returns const iterator
        • std::cbegin() returns const_iterator
      5. NEW: std::decval<T>() returns an rvalue ref to a temporary T object.
        • Advantage is std::declval<T>() doesn't have to call default ctor, which T may not have
      6. Structural binding: [a,b] = std::tie(a, b)
    5. std::declval<T>() returns an lvalue?? , so std::begin() treats it as c&, not c&&?

      • Answer: returns an std::add_rvalue_reference<> type. Reference collapsing is used.
      • So if you pass an lvalue object in, it will be interpreted as lvalue reference.
    6. can also define a struct inside a function.

⚠️ **GitHub.com Fallback** ⚠️