C++ Concepts#
Template and Typename#
The template
and typename
keywords are important to keep in mind when programming. When compiling with Visual Studio they will often not be necessary, and can even be misused in places where they serve no function, but when compiling with GCC or Clang (or almost anything except Visual Studio) they have to be used in their proper place.
Template#
The template
keyword needs to be used any time the
vector<float>
does not need the template keyword because it is not a member type or variable, but Person->ID()
does, as does Person::template Some_Static_Function<ObjectType>()
See cppreference
Typename#
The typename
keyword is necessary for the compiler to better understand template types.
typename
is primarily used with template types to specify that a scope-dependent type is a type rather than a function or variable. By ‘scope-dependent’ we mean a type that is declared with a class or namespace that is being referenced elsewhere - for example, when we use typename MasterType::scenario_type
we need to specify typename
, but when we use MasterType::scenario
the compiler understands we want the object scenario
from that scope.
typename
is also used in template parameters (class
can be used here, too) to declare type parameters. Ex. template <typename T, class C> void Some_Function(T tee, C cee);
See cppreference
CRTP#
virtual
polymorphism has negative performance impacts in our code due to the runtime checking of derived classes. To avoid this slowdown we use static polymorphism (which means the compiler knows at build time which class is which) using the Curiously Recurring Template Pattern (CRTP).
Essentially, when we have a Parent and Child class, we pass the Child class as a template parameter to the Parent class, so that Child derives from itself. Example from Wikipedia:
// The Curiously Recurring Template Pattern (CRTP)
template <class T>
class Parent
{
// methods within Base can use template to access members of Derived
};
class Child: public Parent<Child>
{
// ...
};
SFINAE#
Substitution failure is not an error (SFINAE) is a method for handling template parameters in C++. Essentially, if a type parameter does not pass some template specifier, rather than failing the compiler will attempt to pass it along to another less specific template function of the same definition. Example from Wikipedia:
struct Test {
typedef int foo;
};
template <typename T>
void f(typename T::foo) {} // Definition #1
template <typename T>
void f(T) {} // Definition #2
int main() {
f<Test>(10); // Call #1.
f<int>(10); // Call #2. Without error (even though there is no int::foo)
// thanks to SFINAE.
}
There is not much true SFINAE code left in POLARIS, as it was a holdover from when we used concept-checking to determine the type of a template parameter.
In the modern POLARIS code, SFINAE usually refers to a situation where we have functions that have different parameters types but are otherwise identical. This is not possible when the parameter type is templated, so SFINAE allows us to create one template function that pass along to separate functions below it.
Facade Pattern#
Method Chaining#
Method Chaining refers to the practice of invoking multiple method calls in a single line, using the returned object as a parameter for the next call.
Simple example:
void Start_Bus_Trip(int passenger_count, Area destination)
{
// Set up bus trip with that info
};
int main()
{
// Chain these methods so we don't have to create local variables to store this info
Start_Bus_Trip(Passengers->Return_Count(), Passenger->Find_Destination());
}
Callback#
Callbacks are a generic term for any piece of executable code that is passed to another piece of code. In POLARIS, we use it in instances where polymorphic objects are reduced to their base type (in some containers, for instance) and we can no longer use our CRTP/Prototype-Implementation structures to find the correct function to call. In those instances, a function pointer to the correct call can be saved.
Example:
template <typename V>
struct Vehicle
{
void Get_Fuel(Location* L)
{
// Call the specific function we want
(*get_fuel_callback)(L);
}
void (*get_fuel_callback)(Location* L);
}
struct Electric_Vehicle
{
void Charge_EV(Location* L);
this->get_fuel_callback = &Electric_Vehicle::Charge_EV;
}
struct Diesel_Vehicle
{
void Find_Diesel(Location* L);
this->get_fuel_callback = &Diesel_Vehicle::Find_Diesel;
}
struct Gas_Guzzler
{
void Find_Gas_Station(Location* L);
this->get_fuel_callback = &Gas_Guzzler::Find_Gas_Station;
}
Variant#
std::variant
is another alternative to virtual classes/functions that is especially helpful when we store multiple different types in a container and need to call functions from them.
std::variant
is a special template type that allows us to to create a union of specified types. When we create a variant, we specify the possible types that can be stored in it. To call functions in these classes, we need two things:
All classes in the variant need to have the same function call available (same name, parameters, return, etc.)
A lambda function that we can pass into the std::visit call.
For an example:
struct A
{
float Calculate(float input);
}
struct B
{
float Calculate(float input);
}
int main()
{
using std::variant<A*, B*> as ABvariant;
std::vector<ABvariant> vec;
// fill with both types
for (int i = 0; i < 10; ++i)
{
if (i % 2 == 0)
vec.push_back(new A);
else
vec.push_back(new B);
}
// get the results
float val = 0;
auto lambdaVisitor = [](auto&& _in) { _in->Calculate(val); };
for (auto var : vec)
{
val = std::visit(lambdaVisitor, var);
cout << "val = " << val << endl;
}
}
For more info see cppreference.