Review 03 - Where we start coding¶
Summary¶
Up to this point, we’ve been working through some fundamental of setting up and building code. We haven’t actually made
a program that does anything. I’ve spent a bit of time trying to figure out what our actual review project would be
that is something other than a trivial Hello World
example. I wanted it to be something that is both simple, but
has some relevance.
Since we all have some interest in graphics, I took around to see what’s out there for simple graphics abstractions. I don’t want to go over OpenGL or DirectX just yet; I’ve covered both of those, in C# in other tutorials. This set of reviews is about C++.
There are a lot of options out there:
... the list is quite large. Don’t believe me? Go to Github and search and see the results for yourself. Now, that list includes engines as well as Graphics libraries but I think it illustrates the point; picking a library isn’t a trivial task.
I finally chose Allegro. I’ll try out other libraries as well, but I went with Allegro because:
- It’s got a good NUGet integration with Visual C++: > look here <<
- I’m not looking for 3D or complex drawing facilities.
- I’ve used it way, way, way back, so there’s a bit of familiarity with it.
I don’t have stong opinions on Allegro just yet. I’ve used other wrappers in the past and they all have their strengths and weaknesses. So be adventurous and try a few other libraries on your own!
NB: I may give tinyrender a try, as it illustrates mixing in source files from an external project later.
Allegro setup¶
I’ve already added to this project the Allegro NUGet package. I had to modify the vcproj
by hand, but that was a
trivial thing to do (mostly to get the NUGet package folder correct).
I’m not going to go into detail as to how I set it up - that’s covered in the earlier link to Allegro. That and depending on your OS, you’ll have different requirements. But feel free to use this project as a point of reference if you’re going the Visual C++ route.
One thing I do want to point out is that I am linking the libraries against a single, monolithic static library
.
So what are the options here, and what do they mean.
Simply put, our options are:
- static library
- DLL
- Multithreaded DLL
What are these? What do they mean. I don’t want to assume you all know the difference, so I’ll take a minute to explain (and for more information, you can research further on your own).
Static Library¶
In our previous examples, specifically in Review 2, we were linking against a static library. What this means is that the functions/classes that live in a library are copied from the library and embedded directly into the final executable. That printf function that we used? Remember, that’s not part of the C++ language, it’s a library function that lives in the “C Run-Time” (CRT) library. Different compilers. For the Microsft compiler, that’s the “libcmt*.lib” and “msvcrt*.lib”.
From the Microsoft reference site for libraries here we have the following:
Library | Characteristics | Option | Preprocessor directives |
---|---|---|---|
libcmt.lib | Statically links the native CRT startup into your code. | /MT | _MT |
libcmtd.lib | Statically links the Debug version of the native CRT startup. Not redistributable. | /MTd | _DEBUG, _MT |
msvcrt.lib | Static library for the native CRT startup for use with DLL UCRT and vcruntime. | /MD | _MT, _DLL |
msvcrtd.lib | Static library for the Debug version of the native CRT startup for use with DLL UCRT and vcruntime. Not redistributable. | /MDd | _DEBUG, _MT, _DLL |
I’ve thinned out this table as we don’t want to talk about managed languages yet.
libcmt.lib
and libcmtd.lib
take the machine code, stored in the respective .lib
file and embeds
that into your executable. It’s that simple.
msvcrt.lib
and msvcrtd.lib
inject a ... reference, for want of a better term, into your executable that looks
into a DLL (MSVCP*.dll) for the function to call.
So, why DLLs? Simply put, it allows the author of the library to change the function (to fix issues) or to share the function across multiple applications. Think about it this way - every application that uses printf as a static library has to embed that function into their executable, bloating the size of the executable. You have hundreds (if not thousands) of executables on your machine, each embedding the same functions into the execuatble and you’ve got and incredible amount of duplication of a single function across executables. Putting common functions into a DLL avoids that. It also means that if your function has issues, all you have to do is replace the DLL to get a new version of that function.
But that also runs us into other problems. You’ve no doubt heard the term on windows for ‘DLL Hell’. You might not have a great definition for this - so let me lay out an example for you.
Say you have a vendor that provides application A that installs a set of DLLs. Later, the same vendor might distribute another application, B, that also installs a new set of DLLs, but they haven’t updated application A to use the new DLLs. And a shared function in those DLLs has been updated. It is now completely possible that change has broken application A. So you uninstall application A, which removed the DLL shared with application B and now application B fails to work. And the cycle continues if you reinstall application A ...
It’s not as bad as that, because you can resolve a number of those issues by versioning your DLL, but that comes with its own set of problems. Anyway, the long and the short of it is, for these Reviews, where possible I will be statically linking the executables.
The C/C++ Language - Essentials¶
In the first few reviews, I glossed over a lot of the C++ syntax to focus more on the structure of the build process. I’ll fall back here a bit now and do a very high-level review of the more C like aspects of C++; looking at core language syntax, data types, functions, conditionals and loops.
But first, the code!
// Review03.cpp : Defines the entry point for the application.
//
#include <stdio.h>
#include <allegro5\allegro.h>
#include <allegro5\allegro_image.h>
#include <allegro5\allegro_primitives.h>
#include <allegro5\allegro_font.h>
const int maxiterations = 50;
void DrawFrame(int width, int height);
int main(int argc, char* argv[])
{
al_init();
al_init_font_addon();
al_init_image_addon();
al_init_primitives_addon();
ALLEGRO_DISPLAY* display = al_create_display(800, 600);
ALLEGRO_FONT* font = al_create_builtin_font();
ALLEGRO_EVENT_QUEUE* eventQueue = nullptr;
eventQueue = al_create_event_queue();
al_register_event_source(eventQueue, al_get_display_event_source(display));
al_clear_to_color(al_map_rgb(0, 0, 0));
al_draw_text(font,
al_map_rgb(255, 255, 255),
400, 300,
ALLEGRO_ALIGN_CENTER,
"Welcome to Review03");
while (true)
{
ALLEGRO_EVENT event;
ALLEGRO_TIMEOUT timeout;
al_init_timeout(&timeout, 0.06);
bool get_event = al_wait_for_event_until(eventQueue, &event, &timeout);
if (get_event && event.type == ALLEGRO_EVENT_DISPLAY_CLOSE)
{
break;
}
DrawFrame(800, 600);
al_flip_display();
}
al_destroy_font(font);
al_destroy_display(display);
return 0;
}
void DrawFrame(int width, int height)
{
// Drawing individual pixels in this manner is incredibly slow. This is only for illustration
// on the C syntax.
for (int index = 0; index < maxiterations; index++)
{
al_put_pixel(rand() % width, rand() % height, al_map_rgb(rand()%255, rand()%255, rand()%255));
}
}
And, as an output, we get the following:
C++ Comments¶
Not going to say a lot about comments. There are two types:
- //: Begins a comment which continues until the end of the line. Can be put anywhere in the line.
- /* */: Begins a comment block that starts with the /* and ends with the */. Can start or end anywhere.
Preprocessor Macros¶
When you start with the # symbol, you are beginning a ‘Preprocessor Directive’. Each directive occupies one line (and can be extended across multiple lines using the ` continuation character) and has the following format: - after the `# symbol, you can invoke one of the following commands
- define
- undef
- include
- if
- ifdef
- ifndef
- else
- elif
- endif
- line
- error
- pragma
- you can then add any arguments, based on the aforementioned instruction.
What we are currently doing with the preprocessor is including a header file in lines 4-8 of the example program. These bring in the function signatures as well as other elements defined in the header files. Feel free to peruse the files to see what jumps out at you.
Later, we’ll discuss more about preprocessor macros. But for now, it’s enough to understand that there is more to the preprocessor than just includes.
### Constants
The next line:
const int maxiterations = 50;
defines a variable of type int (integer value) that is constant - it can be set once and cannot be changed after the fact.
We’ll also dig into the const keyword later as it has multiple uses.
### Forward Declarations
Next, we see this:
void DrawFrame(int width, int height);
This tells the compiler that we have a function called DrawFrame the has no return (thus the void in front of the function) and takes two arguments (an integer width and height). Note that this is exactly what you would put into a header file.
### The Entry Point Into Our Application
In C/C++ we define the entry point to our application as:
int main(int argc, char* argv[])
Actually, that’s a bit of a lie. For a ‘console’ application, we define the entry point as above. You can also define the entry point into your application like so:
int main()
or
void main()
int main()
requires you to return an ‘error code’ back to the Operating System. A return value of 0
indicates
no error in program execution. Anything else is an error. These ‘error results’ can be processed by batch files (or
shell scripts) to control batch processing flow. But that discussion is outside of the scope of this article.
void main()
requires no return value. The OS assumes that the program has other means of determining or logging
error conditions.
However, back to the original definition:
int main(int argc, char* argv[])
The two parameters passed into the main
function, argc
and argv
:
argc
- represents the number of ‘arguments’ passed in on the command line.
- we always include the application name in that count
eg:
- app.exe has an argc value of 1
- app.exe /F /S /N has an argc value of 4
argv
- This is an array (that’s what []
represents in C/C++) of char
pointers.
- char *
is a ‘null terminated string’ - it’s the C/C++ way of defining strings.
- All native strings in C/C++ have a null
that defines the end of the string.
- ** this does not include other string types, like STL’s string
.
What does this mean? In the example of the command line looking like this:
app.exe /F /S /N
You can access each element in the command line like so:
array element | value |
---|---|
argv[0] | "app.exe" |
argv[1] | "/F" |
argv[2] | "/S" |
argv[3] | "/N" |
argv[4] | an error |
We’ll go into character strings later. For now, understand that the char C/C++ data type maps to a single byte.
Calling Functions¶
Like printf
, calling a function is pretty straightforward:
al_init();
al_init_font_addon();
al_init_image_addon();
al_init_primitives_addon();
Each of those lines represents a call to an Allegro function. Each of those functions are defined in a header file. They should map to:
Function Name | Header File |
---|---|
al_init | allegro.h |
al_init_font_addon | allegro_font.h |
al_init_primitives_addon | allegro_primitives.h |
Declaring Variables¶
Nothing fancy about the following - we’re just declaring variables:
ALLEGRO_DISPLAY* display = al_create_display(800, 600);
ALLEGRO_FONT* font = al_create_builtin_font();
ALLEGRO_EVENT_QUEUE* eventQueue = nullptr;
OK, one thing that may be a bit odd, if you’re coming from an older version of C++, we have a nullptr
keyword. This
was added into the language spec back in C++ 11. This is a ‘pointer literal’. When we dig into pointers later, we’ll go
over it more, but understand that nullptr
is much more useful to us that null
was. So if your compiler supports
it, use it.
What kinds of variables do we have availble to us in C/C++? There actually aren’t that many:
Type Name | Description |
---|---|
char | A single byte worth of information. This usually maps to the ASCII code table. But not necessarily. |
int | An integer. This can have modifers added to it like unsigned, short and long. |
float | An IEEE floating point number. A great breakdown of it is this. |
double | See the above for a breakdown of a double. |
bool | True or False, 1 or 0, on or off - it’s a boolean! |
void | Represents nothing. Used to define no return value in a function, but also has other (pointer) meanings. |
We can enhance the base types even further using additional keywords:
Classification | Type names | Width (bits) | Notes |
---|---|---|---|
Character types | char |
8 | |
char16_t |
16 | At least as big as a char . |
|
char32_t |
32 | At least as big as a char16_t |
|
wchar_t |
8/16/32 | Wide character - supports the largest character set based on compiler. | |
Signed Integer | short int |
16 | Optimized for space to have at least 16 bits. |
int |
16/32 | First number is the C++ standard definition. Additional number is the max based on specialized compiler. | |
long int |
32/64 | First number is the C++ standard definition. Additional number is the max based on specialized compiler. | |
long long int |
64 | ||
Unsigned Integer | unsigned short int |
16 | Optimized for space to have at least 16 bits. |
unsigned int |
16/32 | First number is the C++ standard definition. Additional number is the max based on specialized compiler. | |
unsigned long int |
32/64 | First number is the C++ standard definition. Additional number is the max based on specialized compiler. | |
unsigned
long long int |
64 | ||
Floating point | float |
32 | float types |
double |
64 | double types | |
long double |
80 | long double types | |
Boolean | bool |
8 | No, it is not 1 bit. Each bool takes up 8 bits. This is why bitmasks/bitflags are useful. |
Miscelleaneous | void |
0 | No data storage for a void. |
nullptr |
nullptr is the same size as a pointer. This can vary. |
We are not limited to just these data types. We can create our own ‘types’ via structures and classes. However, they must be composed, at a bare minimum, of these types.
I’ll leave it to the reader to understand the min/max values that can be stored in each numerical data type.
Loops and Conditionals¶
Just like every language out there, C/C++ has loops and conditionals.
Loops look like this:
// while loop
// while (<iteration condition>)
// {
// // do stuff
// }
int index = 0;
while (index < 10)
{
index = index + 1; // can also be written 'index++;' or '++index;'
}
// for loop
// for ( <init>; <iteration condition>; <expression>)
// {
// // do stuff
// }
for (int counter = 0; counter < 10; counter++)
{
printf("counter: %d\n", counter);
}
// do while loop
// do
// {
// // do stuff
// } while (<iteration condition>)
index = 0; // index was already declared above
do
{
index++;
} while (index < 10)
// More advanced looping structures that we'll cover later
// but show here for a level of completeness, as this was
// introduced in C++ 11 and has a series of additional features.
// This, however, is the simplest case.
int localVector[] = {5, 10, 20, 100, 1024, 5150};
for (int value : localVector)
{
printf("%d ", value);
}
C++ also has conditional statements:
// If condition
// if (<expression evaluates to true>)
// {
// // do stuff
// }
int value = 0;
if (value == 1)
{
// Do stuff
}
// If-Else condition
// if (<expression evaluates to true>)
// {
// // do stuff
// }
// else
// {
// // do something else
// }
if (value == 1)
{
printf("We don't see this\n");
}
else
{
printf("We see this\n");
}
// If-ElseIf condition
// if (<expression evaluates to true>)
// {
// // do stuff
// }
// else if (<expression evaluates to true>)
// {
// // do something else
// }
// else // optional
// {
// // otherwise we do this
// }
value = 3;
if (value == 0)
{
printf("We don't see this\n");
}
else if (value == 1)
{
printf("We don't see this either\n");
}
else
{
printf("We see this\n");
}
// Switch statement
// switch (<expression>)
// {
// case <constant1>:
// { // Brace is optional, but recommended for scoping
// // Do stuff
// }
// break;
//
// case <constant2>:
// { // Brace is optional, but recommended for scoping
// // Do stuff
// }
// break;
//
// case <constantN>:
// { // Brace is optional, but recommended for scoping
// // Do stuff
// }
// break;
//
// default:
// { // Brace is optional, but recommended for scoping
// // Do stuff
// }
// break;
// }
switch (value)
{
case 0:
{
printf("Value is a zero\n");
}
break;
case 1:
{
printf("Value is a one\n");
}
break;
default:
{
printf("I don't know!\n");
}
break;
}
I expect there’s nothing new there for everyone, but I wanted to add it just for completeness sake.
In the code, the only thing I’ll point out is:
while (true)
{
ALLEGRO_EVENT event;
ALLEGRO_TIMEOUT timeout;
al_init_timeout(&timeout, 0.06);
bool get_event = al_wait_for_event_until(eventQueue, &event, &timeout);
if (get_event && (event.type == ALLEGRO_EVENT_DISPLAY_CLOSE))
{
break;
}
In the above, the conditional if (get_event && (event.type == ALLEGRO_EVENT_DISPLAY_CLOSE))
has a &&
in it.
That is the boolean AND
operator. The Boolean OR
operator is defined as ||
.
Please note that there is a difference between &&
and &
as well as ||
and |
. The first, as pointed out
earlier defines a boolean AND
/OR
operation. The latter defines a bitwise AND
/OR
operation. If you
don’t know what a bitwise operation is, we need to talk.
To Summarize¶
That’s one fully functional C/C++ bit of code. We’ve looked at the language from a fairly simple starting point with this bit of code. In the next Review, we’ll look at classes to round out the simple language review. We’ll also talk about different build types and what they’re used for.
With that, I’m out.