Skip to content

Instantly share code, notes, and snippets.

@SeijiEmery
Last active January 5, 2017 00:27
Show Gist options
  • Save SeijiEmery/24f3dd315833b5e81292 to your computer and use it in GitHub Desktop.
Save SeijiEmery/24f3dd315833b5e81292 to your computer and use it in GitHub Desktop.
// 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