C _Design - RicoJia/notes GitHub Wiki
========================================================================
========================================================================
- remove a char using erase-remove idiom:
str.remove(std::erase(str.begin(), str.end(), 'a'), str. end())
-
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).
- 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.
-
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.
- we declared dtor in hpp only, because:
- Notes:
- .hpp
-
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
- 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
- Resource allocation must succeed for resource allocation to succeed. If and only if the object is alive, the resource is being held.
- 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.
- local variables will be destructed only if constructed fully, at the end of the function scope. Therefore, RAII makes memory management easier.
- 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 }
https://sourcemaking.com/design_patterns/factory_method/cpp/2 https://sourcemaking.com/design_patterns/factory_method/cpp/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.
- 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.
}
- Motivation: makes sure only one instance of the object can be created (like cache, etc.).
- Mechanism: static keyword (the object persists.) + private constructor(so nobody can create it outside the class, like using new)
- 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(); }
- Best Practice
- 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 **
-
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. **
-
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());
- 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.
-
Core Mechanisms: referencing/pointers for notifying subscribers with updates & broadcasters for attachment, detachment
-
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
-
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
-
Core Mechanisms: Polymorphism for having different builders, but same overall procedure.
-
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;
}
```
-
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
-
Polymorphism for the adapter tricking the target.
-
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(); }
#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.
- Guidelines:
- 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(); }
- Semaphore - semaphore allows n number of threads to enter, while mutex can only allow one thread to enter.
- 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);
- 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())
- 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++);
}
}
}
- 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
- 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()))
}
- Pretty common to use a queue to transfer data.
========================================================================
========================================================================
-
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.
-
functions with universal reference shouldn't be overloaded, because if you do,
- 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 }
- 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 }
- once you have small things like promotion, the universal ref function is called, because that can yield a match
-
Solution to overloading:
- 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>()); }
- 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 thisclass 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-qualifierstypename = 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"); } }
- use
- Similar to tag dispatch, we use
- 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.
-
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.
- advantages:
-
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)){}
-
for-range loop: need a proper iterator
for (auto a: vec) => for (_begin = vec.begin(); _begin != vec.end(); _beg++) { a = *_begin }
-
enumerate should return a wrapper with
begin(), end()
, and those create an iterator object.- Cuz iterator (not singleton) cannot return an object of itself.
-
std::forward
: to allow for rvalue and lvalue refs -
For the iterator:
- Use SFINAE to tell compiler in compile time that the container does have a
begin()
- so we put
TIter
in template header
- so we put
- And
TIter
depends on type T, so it needs typename -
decltype
: used to return the type of something.- if there's
&&
, or withconst
, will be returned as well
- if there's
-
std::begin(const T& )
(c++11) returns const iterator-
std::cbegin()
returns const_iterator
-
-
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
-
Advantage is
- Structural binding:
[a,b] = std::tie(a, b)
- Use SFINAE to tell compiler in compile time that the container does have a
-
std::declval<T>()
returns an lvalue?? , sostd::begin()
treats it asc&
, notc&&?
- 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.
- Answer: returns an
-
can also define a struct inside a function.
-