Last active
January 5, 2017 00:27
-
-
Save SeijiEmery/24f3dd315833b5e81292 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Quick guide to some C++11 features and good programming practices | |
// Constructors | |
// Suppose we have a class like this, and want to add constructors to it | |
class MyClass { | |
public: | |
// public members and methods... | |
protected: | |
int foo; | |
std::string name; | |
std::vector<SomeComplexObject> objectList; | |
}; | |
// Instead of writing this: | |
class MyClass { | |
public: | |
MyClass (int foo_, std::string name_, const std::vector<SomeComplexObject> & objects) { | |
foo = foo_; | |
name = name_; | |
for (int i = 0; i < objects.size(); ++i) { | |
objectList.push_back(objects); | |
} | |
} | |
}; | |
// Do this: (initializer lists) | |
class MyClass { | |
public: | |
MyClass (int foo, std::string name, const std::vector<SomeComplexObject> & objects) | |
: foo(foo), | |
name(name), | |
objectList(objects) | |
{} | |
}; | |
// The first example actually creates a bunch of temporary objects, since foo, name, and objectList | |
// are initially assigned empty values (whatever that may be), then assigned to (with the assignment | |
// operator) in the main body of the constructor. | |
// The second approach has no main body, and instead initializes the member values with parameters | |
// directly (calling their constructors instead of their assignment operators). Note that the | |
// parameter names don't conflict with the member names in the initializer lists, so you don't have | |
// to do name mangling like in the first approach. | |
// Copy constructors are similar: | |
class MyClass { | |
public: | |
MyClass (const MyClass & other) | |
: foo(other.foo), | |
name(other.name), | |
objectList(other.objectList) | |
{} | |
}; | |
// And there's also move constructors (but that's a bit more complicated... look it up online if you're interested) | |
class MyClass { | |
public: | |
MyClass (MyClass && other) | |
: foo(other.foo), | |
name(std::move(other.name)), | |
objectList(std::move(other.objectList)) | |
{} | |
}; | |
// Destructors are generally very simple - thanks to RAII, your member value's destructors are automatically | |
// called, so there's generally nothing to do unless you're doing manual memory management (BAD!), or want | |
// custom behavior like calling a logging function when your object type goes out of scope. | |
// Either way, they should be declared virtual if *any* method you're using (or a derived method) is virtual. | |
class MyClass { | |
public: | |
virtual ~MyClass () {} | |
}; | |
// Memory semantics | |
// Always do this: | |
void foo (const std::vector<SomeComplexObject> & myObjects) { | |
// do something with objects... | |
} | |
// Instead of this: | |
void foo (std::vector<SomeComplexObject> myObjects) { | |
// ... | |
} | |
// The latter (while default, unfortunately), passes myObjects by *value*, which means that it has to copy the | |
// value passed in (calling its copy constructor) before it can do anything with it. | |
// By passing an object by const reference OTOH, the copy is avoided, although the object becomes immutable. | |
// This is generally a good thing, since this should be what you're doing 90% of the time anyways (you can iterate | |
// over the elements of the list, and do something with those values, but not change them). | |
// If you want to mutate the list and have the changes affect the list you passed in, do this: | |
void foo (std::vector<SomeComplexObject> & myObjects) { | |
myObjects.push_back(SomeComplexObject { 42 }); // this change affects whatever called it | |
} | |
// This also applies for any complex (ie non-primitive) object/value type | |
void foo2 (int x, const SomeComplexObject & obj) { | |
// ... | |
} | |
// The *only* case where you'd want to pass by value is if you want to modify the object passed | |
// in (by making a temporary), but *not* have the changes affect the passed in object. | |
void callMeBob (SomeComplexObject obj) { | |
obj.setName("Bob"); // we don't want to permanently name him bob. | |
cout << "If my name were bob, I'd look like this: " << obj << endl; | |
} | |
// ...or, if you're returning a value – *never* return a local stack value by reference / pointer | |
SomeComplexObject makeABob (...) { | |
SomeComplexObject bob ("bob", ...); | |
return bob; | |
} | |
SomeComplexObject & makeABob (...) { | |
SomeComplexObject bob ("bob", ...); | |
return bob; // bob was created in stack memory (which is about to go out of scope), so the return value is completely undefined | |
} | |
// Also of special note, as of C++11, you can cheaply return a std::vector or any other container type | |
// (uses cheap move semantics, which just swaps pointers, instead of expensive deep-copies) | |
std::vector<int> make1234 () { | |
std::vector<int> values { 1, 2, 3 }; | |
values.push_back(4); | |
return values; | |
} | |
// While I'm at it, C++11 introduced initializer lists, which makes object construction much easier, and | |
// range-based-for, which means that you can do this: | |
void example () { | |
std::vector<std::string> languages { "java", "c++", "python", "brainfuck" }; | |
cout << "my favorite languages:\n"; | |
for (auto language : languages) | |
cout << language << endl; | |
cout << "just kidding xD\n"; | |
} | |
// Instead of this: | |
void example () { | |
std::vector<std::string> languages; | |
languages.push_back("java"); | |
languages.push_back("c++"); | |
languages.push_back("python"); | |
languages.push_back("brainfuck"); | |
cout << "my favorite languages:\n"; | |
for (int i = 0; i < languages.size(); ++i) | |
cout << languages[i] << endl; | |
cout << "just kidding xD\n"; | |
} | |
// Or even worse, this: (which is actually what the range-based for loop does under the hood) | |
void example () { | |
// ... | |
for (std::vector<std::string>::iterator it = languages.begin(); it != languages.end(); ++it) | |
cout << *it << endl; | |
} | |
// If you wanted to iterate in reverse, you'd have to do something similar, but at least can use | |
// automatic type deduction (the auto keyword), which gets rid of that horrible type declaration. | |
void exampleInReverse () { | |
// ... | |
for (auto it = languages.rbegin(); it != languages.rend(); ++it) | |
cout << *it << endl; | |
} | |
// You also can (and should, in some cases) use auto & / const auto & to get pass by reference / const | |
// reference semantics while iterating over an object with a for loop: | |
void finalExample () { | |
// ... | |
for (const auto & language : languages) | |
cout << language << endl; | |
} | |
// If you want to mutate the list you'll have to use reference semantics | |
void add3 (std::vector<int> & values) { | |
for (auto & value : values) | |
value += 3; | |
} | |
void printPrimesPlus3 () { | |
std::vector<int> numbers { 2, 3, 5, 7, 11, 13 }; | |
add3(numbers); | |
for (auto number : numbers) | |
cout << number << ", "; // prints 5, 6, 8, ... | |
} | |
// On pointers: | |
// Avoid, unless you're doing manual memory management and/or pointer arithmetic. | |
// Pointers can do the same thing as references, but which is prettier? | |
// This: | |
void foo () { | |
MyComplexObject obj = // ... | |
mutateThis(obj); | |
thenPrintIt(obj); | |
} | |
void mutateThis (MyComplexObject & obj) { | |
obj.name = "bob"; | |
++obj.nameChangeCount; | |
} | |
void thenPrintIt (const MyComplexObject & obj) { | |
cout << obj << endl; | |
} | |
// Or this? | |
void foo () { | |
MyComplexObject obj = // ... | |
mutateThis(&obj); | |
thenPrintIt(&obj); | |
} | |
void mutateThis (MyComplexObject * const obj) { | |
obj->name = "bob"; | |
++obj->nameChangeCount; | |
} | |
void thenPrintIt (MyComplexObject const * const obj) { | |
cout << *obj << endl; | |
} | |
// The two are functionally identical (including const-correctness semantics), except that the in the | |
// second example the passed in pointer can (hypothetically) be null, and could crash your program when | |
// you try to set (NULL)->name = "bob". | |
// The one good case for using pointers is when you're returning a value that is allowed to be null – eg. | |
// as an alternative to throwing an exception when trying to access an object that may not exist. | |
// This isn't perhaps the best design practice, but it's a valid choice in some cases. | |
Object* MyObjectHandler::getObject (const std::string & name) const { | |
if (myContainer.contains(name)) { | |
return &myContainer[name]; | |
} else { | |
return nullptr; | |
} | |
} | |
void tryToGetFoo (const MyObjectHandler & objectHanlder) { | |
auto foo = objectHandler.getObject("foo"); | |
if (foo != nullptr) { | |
// Foo exists, so do something with it | |
} else { | |
// Do someting else | |
} | |
} | |
// On nullptr: Always use it instead of NULL (as of c++11). The reason is mostly just for standardization, but | |
// the fact is that NULL is really just an integer, and is defined like this: | |
#define NULL ((void*)0) | |
// This is only a problem in a few rare cases, but if you have a set of overloaded functions like this: | |
void foo (const char * str); | |
void foo (int bar); | |
// Calling foo(NULL) calls the second function (the one expecting an integer) instead of the one expecting | |
// the string (which can be NULL), and is the cause of a subtle class of bugs that can crop up when interfacing | |
// with various libraries and legacy code. Pretty rare, but whatever. That aside, the fact is that nullptr is | |
// part of the C++11 spec and NULL isn't, so just get used to it. | |
// On containers: | |
// By far the *most* useful of the std containers are std::vector and std::map. | |
// | |
// The former is a dynamic array, which means that appending elements to the back and performing random access | |
// operations (ie. the index [] operator), is fast (O(1)). Appending to the _front_, or the middle, however – including | |
// delete operations – is quite slow, however (O(n)). In general, trying to delete elements from a std::vector is a bad | |
// idea, and while you can *swap* individual elements quite cheaply, trying to reorder the structure by moving around | |
// chunks of elements will be quite slow (in relative terms, at least: array operations are extremely fast). | |
// | |
// The other two data structures similar to a vector are the list and deque. std::list is the stdlib's implementation of | |
// a linked list, and should be generally avoided. Although they have better performance characteristics than arrays for | |
// insertion (O(1) to delete or add a node link, *if* you already have a pointer to the node itself), their performance | |
// is awful for all other operations (O(n) for indexing operations). They have their uses for *some* applications (eg you're | |
// using them as stacks/queues, or need to tons of element insertions for some reason), but are generally much less efficient | |
// than a vector. std::deque is simply a double ended vector, which means that you can push/pop elements to the front just | |
// as cheaply as elements to the back – if that covers your use case, then use it; otherwise stick with std::vector. | |
// | |
// std::map is an associative array, which maps one type (the key) to another type (the value), and enables you to look up | |
// the values if you have a key. Typically, the key will be a lightweight data type that can be compared with itself (this | |
// property is essential), and it's most common to use std::string or an integer type (typically a unique numeric identifier) | |
// mapped to a more complex (typically user defined) data type. std::map is implemented internally as a btree (specifically, | |
// a red-black tree), so lookup, insertion and deletion are all pretty cheap (O(log(n))), but iteration is a bit more complicated | |
// than with a std::vector. | |
// | |
// Other, related containers are std::set (implements a list with the set property; also implemented internally as a btree), | |
// and std::unsorted_list, which fullfills the same functionality as a std::map, but is implemented as a hashtable. Lookups | |
// and insertions in a hashtable are much faster (O(1)), but they require the key type to be hashable to an integer value | |
// to function whereas std::map simply requires the key type to be comparable (>, !=). | |
// | |
// Now, the main reason to use std::vector is that array operations are extremely fast – iterating over a collection of | |
// like elements located contiguously in memory is quite a bit faster than following pointers in linked list/btree | |
// implementations, *espescially* if there's no branching (modern CPUs are superscalar and try to perform multiple | |
// operations at once with complicated pipelines; branching (conditionals like if statements and function calls) tends to | |
// fuck with that). As such, even though random insertion is quite inefficient with a vector (up to O(n)), those operations | |
// can actually be quite faster with a std::vector than with a linked list which hypothetically has much less algorithmic | |
// complexity, simply because it can be faster to copy N bytes from one memory address to another than to traverse a complex | |
// data structure and swap some pointers (assuming complex copy constructors aren't involved). | |
// | |
// The problem is that to use them correctly, you have to generally treat std::vectors as immutable, and *only* use | |
// indexing, push_back, pop_back, and std::algorithm operations (multiple-removal *can* be done, but it's complex and not | |
// exactly cheap). | |
// So suppose you want to perform a filtering operation where you remove all odd numbers from a list. | |
// Instead of trying to do this: | |
std::vector<int> & removeOdds (std::vector<int> & values) { | |
for (auto i = 0; i < values.size(); ++i) { | |
if (values[i] % 2 != 0) { | |
values.delete(i); // You might wish this operation existed, but it doesn't, and for good reason... | |
} | |
} | |
return values; | |
} | |
// Do this: | |
std::vector<int> filterEvens (const std::vector<int> & values) { | |
std::vector<int> evens; | |
for (auto i = 0; i < values.size(); ++i) { | |
if (values[i] % 2 == 0) | |
evens.push_back(values[i]); | |
} | |
return evens; // dont' do this unless you're using a C++11 compiler that supports move semantics | |
} | |
// This might be counter-intuitive, but iterating over and constructing a new list is actually much faster than | |
// iterating over and trying to modify said list in-place, espescially when using std::vector. | |
// std::map creation w/ brace-based initialization: | |
void example { | |
std::map<std::string, int> wordCount { | |
{ "giraffe", 7 }, | |
{ "oiliphant", 9 }, | |
{ "Sauron", 6 } | |
}; | |
} | |
// The reason that there's a second set of braces is that std::map takes an initializer list of std::pair values – that is, | |
// the constructor we're invoking looks like this: | |
template <typename K, typename V> | |
std::map<K, V>::map (const std::initializer_list<std::pair<K, V>> & values) { | |
for (auto val : values) | |
emplace(val::first, val::second); | |
} | |
// If we wanted to be really verbose, we could write the previous example like this: | |
// (equivalent; C++11 just normally uses type inference to determine what it thinks the types should be) | |
void example () { | |
std::map<std::string, int> wordCount { | |
std::pair<std::string, int> { "giraffe", 7 }, | |
std::pair<std::string, int> { "oiliphant", 9 }, | |
std::pair<std::string, int> { "Sauron", 6 } | |
}; | |
} | |
// This applies to other types as well, and the cool thing is that you can use automatic type deduction | |
// an brace-based initialization *everywhere*. For example, suppose we have a Vec3 class: | |
struct Vec3 { | |
float x, y, z; | |
Vec3 (float x, float y, float z) : x(x), y(y), z(z) {} | |
Vec3 (const Vec3 &v) : x(v.x), y(v.y), z(v.z) {} | |
}; | |
// We can construct a value we're returning with braces: | |
Vec3 operator + (const Vec3 & a, const Vec3 & b) { | |
return Vec3 { a.x + b.x, a.y + b.y, a.z + b.z }; | |
} | |
// And we can ommit the class name too, since we know what type we're returning from the type signature | |
Vec3 operator + (const Vec3 & a, const Vec3 & b) { | |
return { a.x + b.x, a.y + b.y, a.z + b.z }; | |
} | |
// The same applies to passing things as arguments into other functions | |
Vec3 foo (const Vec3 & vec) { | |
// ... | |
} | |
Vec3 doFoo () { | |
return foo({1, 2, 3}) + Vec3 { 4, 5, 10 }; | |
} | |
// This is why static typing is a good thing – if the compiler's actually smart enough to use it (unlike JAVA...), | |
// automatic type deduction can enable very clean, concise code so long as everything's properly annotated somewhere | |
// (Haskell is the best example of this). | |
// To be continued... |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment