Get into GameDev:
cheat-codes for gamedev interviews
━ Additional C++ Topics ━
Introduction
Placement New Operator
Placement New Operator (In-Place New)
The typical new operator returns the amount of requested data from wherever the heap allocator chooses. However, sometimes we want to control where an object is created. Usually this is part of a common strategy where we pre-allocate a large amount of data, sometimes on the stack. Then we can initialize that data wherever we want. C++’s placement-new operator allows us to do this with the following format:
type * myPtr = new( [addr] ) [type] ( [init] )
In the format above, [addr] is the desired destination address, [type] is the variable type (which implicitly communicates its size), and [init] is the initialization argument list. Here’s an example:
int x;
int* myPtr = new( &x ) int ( 10 );
The above example creates a pointer to a new int that is written where x was stored and it initializes the new int with value 10.
New vs Malloc
New and Malloc are c++ keywords that both allocate memory. They have different uses and are paired with unique keywords when it comes to deallocating the data they allocate. Since malloc and free were both supported in C (versus C++) they are sometimes referred to as C-style allocations. It is a common interview question for interviewers to ask you to explain the difference, you should have them memorized.
C-style allocations
- malloc → free mnemonic = “Free Memory”
C++ allocations
- new → delete
- new[ ] → delete[ ]
It is very important to remember that “free” pairs with malloc which I like to remember with the mnemonic “Free Memory” where the F in free stands for Free and the M in Memory stands for malloc. Then you just need to remember that the remaining allocation keywords: new and delete pair together.
So new & delete are the new C++ style allocators but typically, we want to use smart ptrs instead since they are more intelligent and can do things like reference counting.
New:
- new is an operator
- new allocates memory and calls it’s constructor for object initialization.
- Return type of new is exact data type
Malloc:
- malloc() is a library function. ]]
- malloc allocates memory and does not call constructor
- Return type of malloc() is void*
If you can remember that New is the literal newer keyword that only came around for C++ then most of these differences make intuitive sense. C++ is “C with classes” after all so naturally the object oriented functionality like calling constructors and returning object types makes sense to be aspects of the New keyword.
The typical new operator returns the amount of requested data from wherever the heap allocator chooses. However, sometimes we want to control where an object is created. Usually this is part of a common strategy where we pre-allocate a large amount of data, sometimes on the stack. Then we can initialize that data wherever we want. C++’s placement-new operator allows us to do this with the following format:
type * myPtr = new( [addr] ) [type] ( [init] )
In the format above, [addr] is the desired destination address, [type] is the variable type (which implicitly communicates its size), and [init] is the initialization argument list. Here’s an example:
int x;
int* myPtr = new( &x ) int ( 10 );
The above example creates a pointer to a new int that is written where x was stored and it initializes the new int with value 10.
New vs Malloc
New and Malloc are c++ keywords that both allocate memory. They have different uses and are paired with unique keywords when it comes to deallocating the data they allocate. Since malloc and free were both supported in C (versus C++) they are sometimes referred to as C-style allocations. It is a common interview question for interviewers to ask you to explain the difference, you should have them memorized.
C-style allocations
- malloc → free mnemonic = “Free Memory”
C++ allocations
- new → delete
- new[ ] → delete[ ]
It is very important to remember that “free” pairs with malloc which I like to remember with the mnemonic “Free Memory” where the F in free stands for Free and the M in Memory stands for malloc. Then you just need to remember that the remaining allocation keywords: new and delete pair together.
So new & delete are the new C++ style allocators but typically, we want to use smart ptrs instead since they are more intelligent and can do things like reference counting.
New:
- new is an operator
- new allocates memory and calls it’s constructor for object initialization.
- Return type of new is exact data type
Malloc:
- malloc() is a library function. ]]
- malloc allocates memory and does not call constructor
- Return type of malloc() is void*
If you can remember that New is the literal newer keyword that only came around for C++ then most of these differences make intuitive sense. C++ is “C with classes” after all so naturally the object oriented functionality like calling constructors and returning object types makes sense to be aspects of the New keyword.
The Virtual Keyword
The virtual keyword is very overloaded in C++ which means it has many different definitions depending on the context.
Virtual Pages
We already discussed the role of the TLB simulating additional memory capacity using "virtual" pages. See the previous TLB section for more information.
Virtual Methods (and vtables)
Methods can be marked as virtual so that subclasses can override their functionality. Virtual functions add an additional overhead because they are stored in the vTable. Their benefit is sharing the signature from a class to its subclasses. And if pure virtual, ensuring they implement it (as we will discuss later). Any class that has at least one virtual method or virtual base class is polymorphic. Only those types have a virtual method table (VMT) aka "vTable" in their data layout. Every virtual class has a vTable which contains a map from all of its virtual functions to their definitions.
Virtual class memory overhead includes: There is one vTable per object definition (shared by all instances). vTables will have an entry for every virtual function (including pure virtual functions) that its class can call and those entries will each consist of a pointer to the location of the most specific function definition. All the vTables are stored in the static .data segment of memory, specifically .rodata (read-only data).
There is one vPointer per object instance. The vPointer is placed in data at the beginning of the object allocation before any member data. Source: https://stackoverflow.com/questions/1905237/where-in-memory-is-vtable-stored#:~:text=In%20VC%2B%2B%2C%20the%20vtable,from%20other%20classes%20with%20vtables.
I should mention that the C++ standard does not define how virtual functions are actually implemented, only how they should behave. So the exact virtual memory layout may vary by compiler but the above layout is what we should expect to see.
The linker uses the first non-inline, non-pure, virtual function declared in the class in order to find the class' vTable, this function is called the key function.
Virtual Pages
We already discussed the role of the TLB simulating additional memory capacity using "virtual" pages. See the previous TLB section for more information.
Virtual Methods (and vtables)
Methods can be marked as virtual so that subclasses can override their functionality. Virtual functions add an additional overhead because they are stored in the vTable. Their benefit is sharing the signature from a class to its subclasses. And if pure virtual, ensuring they implement it (as we will discuss later). Any class that has at least one virtual method or virtual base class is polymorphic. Only those types have a virtual method table (VMT) aka "vTable" in their data layout. Every virtual class has a vTable which contains a map from all of its virtual functions to their definitions.
Virtual class memory overhead includes: There is one vTable per object definition (shared by all instances). vTables will have an entry for every virtual function (including pure virtual functions) that its class can call and those entries will each consist of a pointer to the location of the most specific function definition. All the vTables are stored in the static .data segment of memory, specifically .rodata (read-only data).
There is one vPointer per object instance. The vPointer is placed in data at the beginning of the object allocation before any member data. Source: https://stackoverflow.com/questions/1905237/where-in-memory-is-vtable-stored#:~:text=In%20VC%2B%2B%2C%20the%20vtable,from%20other%20classes%20with%20vtables.
I should mention that the C++ standard does not define how virtual functions are actually implemented, only how they should behave. So the exact virtual memory layout may vary by compiler but the above layout is what we should expect to see.
The linker uses the first non-inline, non-pure, virtual function declared in the class in order to find the class' vTable, this function is called the key function.
Practice Question: Draw the vTable(s) and vPointer(s) that would be constructed at runtime for the following program.
Class B{
public:
virtual Void Hello(){}
virtual Void Howdy(){}
};
class D: public B {
public:
virtual Void Hello(){}
virtual Void Goodbye(){}
};
int main(int argc, char* argv[]) {
D *d1 = new D();
D *d2 = new D();
B *b1 = new B();
return 0;
}
We will see a vTable for each class definition.
Each of the vTables will have one entry for each of their virtual methods.
B's vTable --> [ &B:Hello, &B:Howdy ]
D's vTable --> [ &D:Hello, &B:Howdy, &D:Goodbye ]
We will see a vPointer for each instanced class.
d1's vPointer --> D's vTable
d2's vPointer --> D's vTable
b1's vPointer --> B's vTable
This question was inspired by the following SO question:
https://stackoverflow.com/questions/23170175/how-many-vtable-and-vpointers-will-be-created-in-this-example
Dynamic vs Static Dispatch
Static dispatching refers to a compiler knowing exactly what function definition will be used for a method call. Dynamic dispatch is a generic way to refer to the process of determining which function definition a method is referencing at runtime. Virtual method tables are an example of how to implement dynamic dispatch. Marking a method as virtual is telling your compiler to dynamically dispatch the method call. We can deliberately perform a static dispatch by specifying the class type before the method invocation such as ClassName::methodName();
Will virtual function calls be inlined?
Inlining is a compiler optimization wherein the compiler adds the code of a method directly into the code where the method is called. We will discuss Inlining more further into this chapter but it is important to know now that virtual functions will only sometimes be eligible for inlining. Whenever a virtual function is called using a base-class-reference or pointer, it cannot be inlined (because the call is resolved at runtime). But whenever a virtual function is called using the object of that class, it can be inlined because compiler knows the exact class of the object at compile time.
Virtual Base Classes
If a class inherits from two classes that are each subclasses of a shared base class, then that shared base class' data members will be duplicated when we try to inherit from each subclass. To fix this issue we need to declare the subclasses using the virtual keyword. Example:
class SubClass1 : virtual public BaseClass {};
class SubClass2 : virtual public BaseClass {};
class NewClass : SubClass1, SubClass2{};
Demo: www.youtube.com/watch?v=acEkaZvnjCg&ab_channel=Codearchery
Other notes:
Practice Question: Imagine you have a class named A which implements a method, and A has two derived "sub-classes" B1 and B2which both override the A's implementation of the method. Additionally, let's imagine that a fourth class exists called C which is both a subclass of B1 and B2. When C calls the method, will it use B1's definition of the method or B2's? How can this design be improved?
This is a classic problem called the Diamond Inheritance problem, so called because the class hierarchy can be drawn as a diamond with A at the top, B1 and B2 in the middle, and C at the bottom. The result of this situation is that A's data members are inherited twice into C (once from B1 and once from B2).
C++ requires you to specify in the call to C's method whether the implementation of B1 or B2 should be used. The invocation looks something like this: B1::C.Method() which indicates that we should call B1's override of the method. This resolves the ambiguity but it does not resolve the data duplication.
To prevent duplicating A's data, you need to define subclass B with virtual inheritance which looks like this: class B1 : virtual public A{ }. This will prevent data member duplication as long as both B1 and B2 are defined with virtual inheritance. In general, it's a good practice to use virtual inheritance whenever you are using a derived class as a bass class.
Some studios choose to outright ban multiple inheritance to simply prevent these sorts of scenarios.
C++ requires you to specify in the call to C's method whether the implementation of B1 or B2 should be used. The invocation looks something like this: B1::C.Method() which indicates that we should call B1's override of the method. This resolves the ambiguity but it does not resolve the data duplication.
To prevent duplicating A's data, you need to define subclass B with virtual inheritance which looks like this: class B1 : virtual public A{ }. This will prevent data member duplication as long as both B1 and B2 are defined with virtual inheritance. In general, it's a good practice to use virtual inheritance whenever you are using a derived class as a bass class.
Some studios choose to outright ban multiple inheritance to simply prevent these sorts of scenarios.
Pure Virtual Methods
Pure virtual functions aka "abstract" functions are just virtual functions that do not declare an implementation. Pure virtual functions must be implemented by subclasses.
Below is an example pure virtual definition. Interviews often ask you to write out a pure virtual function definition like this example.
class Base {
public:
virtual void functionName() = 0;
};
Note that the above example is only showing the header. This pure virtual function may have a body defined in an implementation as explained below.
A virtual method makes a class abstract and forces its children to implement it or they’re also abstract. The method is not 'abstract' itself, pure virtual methods can define bodies. C++ doesn't really have 'abstract methods' like other languages. As mentioned previously, the closest we come to an abstract method would be a pure virtual method with no body. Derived classes that implement a pure virtual function may call its implementation somewhere in their code.
Scott Meyer's advice: "If part of the code of two different derived classes is similar, then it makes sense to move it up in the hierarchy, even if the function should be pure virtual." In this way C++ is a bit more malleable than other languages like, say, C#. In C#, you cannot define a body for an abstract method so you are forced to either providing a shared definition for subclasses or mandating the method is implemented in subclasses. But C++ allows you to do either of these options: marking a method as pure virtual is the mandate and yet you can still define a body for subclasses to share.
A program will never dynamically dispatch to a pure virtual function. This is because if a class has a pure virtual method then it is an abstract class. Since it is abstract, it will never be instanced. If a subclass of the abstract class exists, then it will have to define an implementation for any abstract methods and those implementations will be dynamically dispatched to instead of the abstract class' implementation (if any). Pure virtual methods are only statically dispatched (invoked manually). Despite never dynamically dispatching to a pure virtual method, a vTable will have an entry for each pure virtual method in its associated class. These entries typically point to a generic fail-state method defined by the compiler.
Practice Question: What is the difference between the vtable for a class with virtual functions and a class with pure virtual functions?
vTable entries for pure virtual functions will point to a generic compiler-defined failure function.
https://stackoverflow.com/questions/7636230/c-interview-vtable-for-a-class-with-a-pure-virtual-function
https://stackoverflow.com/questions/7636230/c-interview-vtable-for-a-class-with-a-pure-virtual-function
Virtual Destructors
You need to mark a class' destructor as virtual if you will destruct a derived object of that class using a pointer with the type of its base class. Without a virtual destructor memory will leak, here's an example:
If Pet p = Dog, and I delete p (which has no virtual destructor), it will destruct the Pet data but not propagate the call down to Dog's destructor. The Dog class may have had memory that is now leaked!
This is an extremely common interview topic; you should memorize that...
*Technically* your method does not need a virtual destructor if:
- Your class is never instantiated on the heap
- You have no intention to derive classes from the class
- You never refer to an instance of a derived class using a pointer of type baseClass
I emphasized "technically" because in practice you should probably implement virtual destructors a bit conservatively, just in case its usage changes over time. The wise Scott Meyers advises: "if a class has any virtual function, it should have a virtual destructor unless it’s not designed to be a base class or not designed to be used polymorphically."
Static Keyword
The static keyword can be used in context of static classes, static functions, and static variables but in all of these cases the keyword means the same thing: one-time allocation in a static storage area within scope for the entire duration of the program.
Static Classes
A static class cannot be instantiated, and can contain only static members. Stored in static area (not on stack).
Static Functions
Static functions within a class are instanced only once and shared by all instances of the class. Because there is no class instance associated with a static function you can access it without needing a class instance.
It's unique properties are that...
Static Variables
Static variables within a class are instanced only once and shared by all instances of the class. Because there is no class instance associated with a static variable you can access it without needing a class instance. These are basically global but there is an organizational benefit of putting it into a class as static if you feel the variable is strongly associated with that class.
Primitive static variables that are not const-initialized are always zero initialized. In the below example, I have set the names of some variables to contain the substring “zeroinit” to convey that they will be zero initialized and I named one "undef" to convey that it will be undefined.
static int zeroInit1;
int zeroInit2;
int main(){
int undef;
static int zeroInit3;
}
Noticed that even though zeroInit2 is not static, it is still zero initialized. This is because it is a class variable. Remember that zero initialization is only for primitive types, there is no zero-initialization for user-defined types.
Static Classes
A static class cannot be instantiated, and can contain only static members. Stored in static area (not on stack).
Static Functions
Static functions within a class are instanced only once and shared by all instances of the class. Because there is no class instance associated with a static function you can access it without needing a class instance.
It's unique properties are that...
- It can’t directly access the non-static members of its class
- It can’t be declared const, volatile or virtual.
- It doesn’t need to be invoked through an object of its class, although for convenience, it may. Usually, static member functions are called using the class name: ClassName::function( )
Static Variables
Static variables within a class are instanced only once and shared by all instances of the class. Because there is no class instance associated with a static variable you can access it without needing a class instance. These are basically global but there is an organizational benefit of putting it into a class as static if you feel the variable is strongly associated with that class.
Primitive static variables that are not const-initialized are always zero initialized. In the below example, I have set the names of some variables to contain the substring “zeroinit” to convey that they will be zero initialized and I named one "undef" to convey that it will be undefined.
static int zeroInit1;
int zeroInit2;
int main(){
int undef;
static int zeroInit3;
}
Noticed that even though zeroInit2 is not static, it is still zero initialized. This is because it is a class variable. Remember that zero initialization is only for primitive types, there is no zero-initialization for user-defined types.
Practice Question: Here are some good static key word questions: https://www.geeksforgeeks.org/c-static-keyword-question-1/#
Autogenerated Functions
Six functions are auto-generated by compiler, by default they’re all public and you need to memorize them.
Destructor / Constructor, Move methods (MC and MA) and Copy methods (CC and CA).
So if you can remember what two methods correspond to each group, remembering the group names is as easy as “DC and Marvel Comics”.
When are they not auto generated:
The six auto generate functions will not be auto generated in the following cases:
Rule of Three: if a class defines any of the following then it should probably explicitly define all three:
Assignment Operators
Some considerations for when you are implementing your own assignment operator.
- DC - default constructor - Foo::Foo(){}
- CC - copy constructor - Foo (Foo const&){}
- CA - copy assign. op. - Foo & operator=( Foo const&) = delete;
- D - destructor - Foo::~Foo(){}
- MC - move constructor - Foo(Foo&&); ← hollows RHS obj into this obj // for C++ 11+ (link)
- MA - move assignment operator - Foo& operator=(Foo&&); // for C++ 11+ (link)
Destructor / Constructor, Move methods (MC and MA) and Copy methods (CC and CA).
So if you can remember what two methods correspond to each group, remembering the group names is as easy as “DC and Marvel Comics”.
When are they not auto generated:
The six auto generate functions will not be auto generated in the following cases:
- When you implemented them explicitly
- When you prevent them explicitly : Foo(Foo const&) = delete;
- When compiler can't, such as if Dog is-a Pet and Pet has no copy constructor. Dog is not going to automatically have a copy constructor because it would not know how to deal with Pet’s members during a copy.
Rule of Three: if a class defines any of the following then it should probably explicitly define all three:
- destructor
- copy constructor
- copy assignment operator
Assignment Operators
Some considerations for when you are implementing your own assignment operator.
- An assignment operator is only called if the object has already been constructed, otherwise the constructor is called.
- The assignment operator needs to free allocated memory that was previously held by an object. This is unnecessary in the constructor because no memory has been reserved yet if the object has not yet been constructed. This is the key reason we need two operators. The only exception being if we for some reason called ther assignment operator on itself in which case we just do nothing as explained here.
Casting
You are expected to memorize the following four C++ cast types. To remember them I use the mnemonic: “Coders & Designers Rarely Sleep”
C-style cast − Lastly, a C-Style cast should generally not be used, but the way it works is that it tries some of the above, ending with the reinterpret_cast. Note that dynamic_cast is never considered when using a C-style cast.
- const_cast− used to override the const and/or volatile keyword.
- Modifying a previously const var is undefined but sometimes legal (compiler-dependent)
- Usually indicative of a design flaw, we shouldn’t be casting to and from const.
- dynamic_cast − used to polymorphically convert pointers and references
- No cast is needed to convert from subclass to base class. You only need to dynamic cast if converting from a base class to a subclass. To remember this principle, I use the mnemonic “one must pay to go down”, and I imagine a man charging children money for them to go down a slide.
- If you try to dynamic_cast a non-polymorphic type, it will work exactly as a static cast.
- Only valid if RTTI is enabled for the compiler
- reinterpret_cast − R for “Risky”, c-style, no warnings, not type-safe
- Unnecessary, do not use (except maybe for from a void* that you might get returned from malloc)
- static_cast − S for “Standard”, c-style with warnings, normal type conversions, no runtime checks, only static info
C-style cast − Lastly, a C-Style cast should generally not be used, but the way it works is that it tries some of the above, ending with the reinterpret_cast. Note that dynamic_cast is never considered when using a C-style cast.
- const_cast
- reinterpret_cast, then const_cast (change type + remove const)
- reinterpret_cast
- static_cast, then const_cast (change type + remove const)
- static_cast
Pointers
Smart Pointers
These are the three kinds of smart pointers you need to memorize.
std::unique_ptr - when you only need this one reference to the object. (this replaced auto_ptr)
std::shared_ptr - when you do not want de--allocation until several references to it are gone.
Dumb pointers vs Smart pointers
Pass-by-reference vs Pass-by-value vs Pass-by-pointer
Pass-by-references is more efficient than pass-by-value, because it does not copy the arguments. The formal parameter is an alias for the argument. When the called function read or write the formal parameter, it is actually read or write the argument itself.
The difference between pass-by-reference and pass-by-value is that modifications made to arguments passed in by reference in the called function have effect in the calling function, whereas modifications made to arguments passed in by value in the called function can not affect the calling function. Use pass-by-reference if you want to modify the argument value in the calling function. Otherwise, use pass-by-value to pass arguments.
The difference between pass-by-reference and pass-by-pointer is that pointers can be NULL or reassigned whereas references cannot. Use pass-by-pointer if NULL is a valid parameter value or if you want to reassign the pointer. Otherwise, use constant or non-constant references to pass arguments.
Source: https://www.ibm.com/docs/en/zos/2.4.0?topic=calls-pass-by-reference-c-only
References vs Pointers
Reference
- Cannot be reassigned
- Cannot be null
- Does not have its own address
Pointer
- It's a new variables with its own memory & size
- Supports pointer arithmetic “pointer++”
These are the three kinds of smart pointers you need to memorize.
std::unique_ptr - when you only need this one reference to the object. (this replaced auto_ptr)
std::shared_ptr - when you do not want de--allocation until several references to it are gone.
- Drawbacks: because they need to increment atomically they must lock if shared between threads, which could be a performance issue.
Dumb pointers vs Smart pointers
- Smart pointers do not support pointer arithmetic
Pass-by-reference vs Pass-by-value vs Pass-by-pointer
Pass-by-references is more efficient than pass-by-value, because it does not copy the arguments. The formal parameter is an alias for the argument. When the called function read or write the formal parameter, it is actually read or write the argument itself.
The difference between pass-by-reference and pass-by-value is that modifications made to arguments passed in by reference in the called function have effect in the calling function, whereas modifications made to arguments passed in by value in the called function can not affect the calling function. Use pass-by-reference if you want to modify the argument value in the calling function. Otherwise, use pass-by-value to pass arguments.
The difference between pass-by-reference and pass-by-pointer is that pointers can be NULL or reassigned whereas references cannot. Use pass-by-pointer if NULL is a valid parameter value or if you want to reassign the pointer. Otherwise, use constant or non-constant references to pass arguments.
Source: https://www.ibm.com/docs/en/zos/2.4.0?topic=calls-pass-by-reference-c-only
References vs Pointers
Reference
- Cannot be reassigned
- Cannot be null
- Does not have its own address
Pointer
- It's a new variables with its own memory & size
- Supports pointer arithmetic “pointer++”
Miscellaneous C++ Keywords & Vocabulary
A portion of most gamedev coding interviews will judge your familiarity with C++, these question will often ask for the definition and use of the following keywords. Review each and ensure you are familiar with its proper usage.
Volatile: “Omit from optimization”
Friend: the friend keyword is used to declare one class to be a friend of another. Classes can access the private and protected members of any of their friend classes.
private:
friend class OtherClassName;
};
In the above example OtherClassName is declared as a friend of ClassName. The declaration is within the private access specifier but that is irrelevant, access specifiers have no impact of friend declarations.
Const: const is an extremely common keyword that determines if a variable will change its value. Usually the const keyword precedes a type in an initialization statement. For example:
const int x;
The above line means x cannot change the value of its integer. However, in the case of pointers, the const keyword can be placed in several different positions, such as in the below example the const keyword can be place in either the A1 or A2 position and it can also be placed in the B position:
A1 int A2 * B myPtr
Let's break down what the const value would mean in each of these positions:
Reflection: reflection refers to runtime compiling, and could be useful for compiling a code class based on art data, or perhaps a property field display in a component editor. Unity uses reflection to inspect classes for an Update method.Linked: 1, 2,
Explicit: forces object creation to use the constructor. Prevents implicit conversions that would be caused (for example, putting an int when it expects a customNum where customNum has an int constructor defined).
https://www.youtube.com/watch?v=Rr1NX1lH3oE&ab_channel=TheCherno
Extern: prevents compiler name mangling
Include vs Using:
C++ Generation steps: Source Link
Inline: suggests to compiler that a function should be inlined. Inlining is when instead of setting up a function call the compiler pastes the function's contents directly into the area of the code that called the function. The benefits of inlining are...
See the virtual section above for a discussion on inlining virtual functions.
Union: a user-defined datatype that groups values so that they can all share one value. Useful for shared storage of conditionally necessary variables. Usually used with a variable to indicate which type is in use.
RAII ( Resource Acquisition Is Initialization ): a C++ programming technique which binds the life cycle of a resource that must be acquired before use to the lifetime of an object.
RTTI (run time type info):
Interface: C++ doesn’t have Java-like interfaces, but but the closest equivalent would be an abstract class with only pure virtual methods (with no bodies) and only static const data members. C++, unlike Java and C#, has multiple inheritance which will allow you to implement different interfaces LINK. You could then use is_base_of() to assess if an interface is implemented.
Volatile: “Omit from optimization”
Friend: the friend keyword is used to declare one class to be a friend of another. Classes can access the private and protected members of any of their friend classes.
- Friendship is not transitive (friends of friends are not friends).
- Friendship is not necessarily bi-directional (just because A is a friend of B, B is not automatically a friend of A).
- Friendship is not inherited (subclasses of friends do not become friends automatically).
private:
friend class OtherClassName;
};
In the above example OtherClassName is declared as a friend of ClassName. The declaration is within the private access specifier but that is irrelevant, access specifiers have no impact of friend declarations.
Const: const is an extremely common keyword that determines if a variable will change its value. Usually the const keyword precedes a type in an initialization statement. For example:
const int x;
The above line means x cannot change the value of its integer. However, in the case of pointers, the const keyword can be placed in several different positions, such as in the below example the const keyword can be place in either the A1 or A2 position and it can also be placed in the B position:
A1 int A2 * B myPtr
Let's break down what the const value would mean in each of these positions:
- If const is placed at location B then the pointer is constant. That means we cannot change which memory address the pointer is pointing to, but it does not restrict us from changing the value located at that address. If const is placed on B then reassigning myPtr's value such as in the following example will produce an error:
- myPtr = newPtr
- If const is placed at location A1 or A2 then the value is constant. That means we cannot change the value pointed to by the pointer, but it does not restrict us from changing the which address the pointer is pointing to. Const cannot be placed on both A1 and A2, one position must be chosen and most game studios choose either the A1 or A2 position to be the definitive position to use for all of their values. If const is placed on A1 or A2 then reassigning the value myPtr is pointing at such as in the following example will produce an error:
- *myPtr = newValue
Reflection: reflection refers to runtime compiling, and could be useful for compiling a code class based on art data, or perhaps a property field display in a component editor. Unity uses reflection to inspect classes for an Update method.Linked: 1, 2,
Explicit: forces object creation to use the constructor. Prevents implicit conversions that would be caused (for example, putting an int when it expects a customNum where customNum has an int constructor defined).
https://www.youtube.com/watch?v=Rr1NX1lH3oE&ab_channel=TheCherno
Extern: prevents compiler name mangling
- extern "C" { void foo(); }
Include vs Using:
- 'Include' basically does a copy-paste of the value of a file to the location of the "include" line. This is used to make your source code (usually .c file) aware of a declaration of other source code (usually sits in .h file).
- 'Using' basically tells the compiler that in the next code you are using something (usually a namespace) so you won't have to do it explicitly each time.
- Additional Info:
C++ Generation steps: Source Link
- Pre-Processor: provides directives for the compiler. Handles #define's, #include's, and macros.
- Compiler: reads, analyses and translates code into either an object file or a list of error messages. Generates .OBJ from .CPP
- Linker: combines one or more object files and possibly some library code into either a executable, a library or a list of error messages. Generates .EXE from .OBJ
- Loader: reads the executable code into memory, does some address translation and tries to run the program resulting in a running program or an error message (or both).
Inline: suggests to compiler that a function should be inlined. Inlining is when instead of setting up a function call the compiler pastes the function's contents directly into the area of the code that called the function. The benefits of inlining are...
- Reduces the overhead associated with setting up and executing a function.
- Prevents jumping to a different area of instructions, leading to better instruction cache locality.
- The compiler can better optimize its instruction scheduling. This is easier to do with an inlined function than trying to optimized the instruction scheduling of two separate functions ( so called inter-procedural optimizations ).
See the virtual section above for a discussion on inlining virtual functions.
Union: a user-defined datatype that groups values so that they can all share one value. Useful for shared storage of conditionally necessary variables. Usually used with a variable to indicate which type is in use.
RAII ( Resource Acquisition Is Initialization ): a C++ programming technique which binds the life cycle of a resource that must be acquired before use to the lifetime of an object.
RTTI (run time type info):
- Allows dynamic casting
- Allows exception handling
- Allows std::any ← can be any type
Interface: C++ doesn’t have Java-like interfaces, but but the closest equivalent would be an abstract class with only pure virtual methods (with no bodies) and only static const data members. C++, unlike Java and C#, has multiple inheritance which will allow you to implement different interfaces LINK. You could then use is_base_of() to assess if an interface is implemented.
C++ Build Steps
The following steps happen for each C or CPP file.
- Preprocessing - Instructs the compiler to do required pre-processing before the actual compilation. You can call this phase Text Substitution or interpreting special preprocessor directives denoted by #. The resulting .I files combine .CPP files with any .H files they include. Each of these CPP files is considered a translation unit.
- Compilation - Translates the program into another targeted language. If there is some errors, the compiler will detect them and report it. Outputs assembly for the Assembler.
- Assemble - Assembly code gets translated into machine code.
- Linking - If the code needs some other source file to be linked, the linker links them to make it a executable file.
- Loader - It loads the executable code into memory; program and data stack are created, register gets initialized.
Move Semantics
To start, it's important to understand the difference between an lvalue and an rvalue. This video is a good primer for the role of lvalues and rvalues.
Basic definitions:
An rvalue is...
Rules to remember:
Move semantics were introduced in 2011 as part of C++11 and in some circumstances they can remove the need of unnecessarily copying data. Instead of copying it (aka copy semantics) we 'move' it (hence the term move semantics). 'Move' specifically refers to changing the ownership of an object's data which means taking its pointers and copying the pointers rather than the underlying data.
This video does a good job of explaining how move semantics work in practice. The following example function from the video shows a move constructor which moves the input object's data into the local object and then empties the input object's data. Rather than copying the contents of the string, we are able to just copy m_Data which is a pointer to the contents.
String( String&& other ) {
m_Size = other.m_Size;
m_Data = other.m_Data;
other.m_Size = 0;
other.m_Data = nullptr;
}
The move constructor shown above takes an rvalue reference as an input parameter. In order to use that function, we need to first take the rvalue we want to pass in and cast it to an rvalue reference; the std::move function does the cast for us. As explained in this article, std::move, despite its name, does not move anything. std::move merely casts its argument to an rvalue reference to allow moving it, but doesn't guarantee a move operation.
In this video, the following lines are used to invoke the above function:
String sourceString = "Hello";
String destString( std::move( sourceString ) );
Additional reading:
https://stackoverflow.com/questions/17642357/const-reference-vs-move-semantics
https://stackoverflow.com/questions/3413470/what-is-stdmove-and-when-should-it-be-used
Perfect Forwarding
As explained in this video, a function uses perfect forwarding to forward arguments without changing their lvalue or rvalue characteristics. In the video example, perfect forwarding combines move semantics, variadic templates, and the in-place new operator to prevent a constructor call. Again the main idea here is simply that if we have an rvalue as an input parameter then we are also sending an rvalue as an input parameter to the next function we are calling. Hence the name 'perfect' implying that we have not changed anything about the parameter, not even it's l/r value status.
Basic definitions:
An rvalue is...
- a temporary objects.
- an objects without names.
- an objects which have no address.
- an object that has an identifiable location in memory
Rules to remember:
- You cannot set an lvalue reference to be an rvalue. For example this is not allowed: int& a = 10;
- You can set a const lvalue reference to be an rvalue: For example this is allowed: const int& a = 10;
- rvalue references have two ambersands instead of one. For example this function takes an rvalue reference: void Foo( int&& bar);
- rvalue references are used to restrict functions to only take in temporary values such as the result of a string addition.
Move semantics were introduced in 2011 as part of C++11 and in some circumstances they can remove the need of unnecessarily copying data. Instead of copying it (aka copy semantics) we 'move' it (hence the term move semantics). 'Move' specifically refers to changing the ownership of an object's data which means taking its pointers and copying the pointers rather than the underlying data.
This video does a good job of explaining how move semantics work in practice. The following example function from the video shows a move constructor which moves the input object's data into the local object and then empties the input object's data. Rather than copying the contents of the string, we are able to just copy m_Data which is a pointer to the contents.
String( String&& other ) {
m_Size = other.m_Size;
m_Data = other.m_Data;
other.m_Size = 0;
other.m_Data = nullptr;
}
The move constructor shown above takes an rvalue reference as an input parameter. In order to use that function, we need to first take the rvalue we want to pass in and cast it to an rvalue reference; the std::move function does the cast for us. As explained in this article, std::move, despite its name, does not move anything. std::move merely casts its argument to an rvalue reference to allow moving it, but doesn't guarantee a move operation.
In this video, the following lines are used to invoke the above function:
String sourceString = "Hello";
String destString( std::move( sourceString ) );
Additional reading:
https://stackoverflow.com/questions/17642357/const-reference-vs-move-semantics
https://stackoverflow.com/questions/3413470/what-is-stdmove-and-when-should-it-be-used
Perfect Forwarding
As explained in this video, a function uses perfect forwarding to forward arguments without changing their lvalue or rvalue characteristics. In the video example, perfect forwarding combines move semantics, variadic templates, and the in-place new operator to prevent a constructor call. Again the main idea here is simply that if we have an rvalue as an input parameter then we are also sending an rvalue as an input parameter to the next function we are calling. Hence the name 'perfect' implying that we have not changed anything about the parameter, not even it's l/r value status.
Unsorted Topics
Q: What are the differences between a C++ struct and C++ class?
The only differences are that a struct defaults to public member access and public base-class inheritance, and a class defaults to the private access specifier and private base-class inheritance. Structs can have inheritance.
Forward Declaration
Useful if we just need to know an object exists, but we don’t need to use its properties. Note, you still need the A.h include in your B.cpp file. As shown below, a forward declaration is sufficient if you just need to reference a pointer to a class, but if you need to access the class value then you will get a compiler error as a full include is needed and a forward declaration is not sufficient.
//File A.h
class A{
};
// File B.h
class A; //forward declaration
class B {
A* m_pTest; // no error
A m_Test; //COMPILER ERROR
};
Common reasons why bugs in a release version may not be present in a debug version of the same program
Multi file compilation
Misc. questions
The only differences are that a struct defaults to public member access and public base-class inheritance, and a class defaults to the private access specifier and private base-class inheritance. Structs can have inheritance.
Forward Declaration
Useful if we just need to know an object exists, but we don’t need to use its properties. Note, you still need the A.h include in your B.cpp file. As shown below, a forward declaration is sufficient if you just need to reference a pointer to a class, but if you need to access the class value then you will get a compiler error as a full include is needed and a forward declaration is not sufficient.
//File A.h
class A{
};
// File B.h
class A; //forward declaration
class B {
A* m_pTest; // no error
A m_Test; //COMPILER ERROR
};
Common reasons why bugs in a release version may not be present in a debug version of the same program
- Variables are often null-initialized in debug but not in release
- Certain code may only run in debug mode, for example asserts are cut for release. Additionally your program may have other #ifdef logic that is only encountered in one of the versions
- The debug version may use a debug version of a library function such as a debug memory allocation
- The debug environment may have access to more memory in the test environment than on the release platform (something like the developer kit version of an xbox will have more memory than the consumer version).
- The debug and release project configurations of an IDE may have different settings
Multi file compilation
Misc. questions
- https://www.codingame.com/work/cpp-interview-questions/
- Be sure to checkout the "additional reading" chapter of this book for more resources!
Practice Question: Given the following declaration (in a .h probably) and initializer list (in a .cpp probably) what is the order of initialization? A first, or B first?
Header
MyType B;
MyType A;
Implementation
Foo() : A(), B() {}
Header
MyType B;
MyType A;
Implementation
Foo() : A(), B() {}
B will always get initialized first, the header declaration order is what matters... even though the initializer list makes it look like A gets initialized first. This is confusing so many studios (and some compilers like GCC) enforce that the order of header declarations is matches by the order of the initializer list.