Hello again, Medium! Last week, we took a peek at the const keyword and constant expressions in C++.
Today (after much reading and boring topics), we’ll be looking at internal and external linkage in C++. We’ll also delve a bit into global variables and variable declarations to understand the dangers behind excessive usage of global variables.
Let’s get started.
extern const double Time { 12.54 };
Let’s quickly gloss over some definitions before getting to some examples. Internal and external linkage boil down to the file(s) that a specific variable can be used.
By default, global variables possess external linkage, which means we can use them in any file we desire (across multiple source files), provided we forward declare the variable using the extern keyword.
Consider the following example:
// extern.cpp
int g_extern { 12 };
// main.cpp
#include <iostream>
// this is a global variable, we use the 'g_' prefix to denote that
int g_variable { 6 };
// this is a forward declaration of a global variable in a different file!
extern int g_extern;
int main()
{
std::cout << g_variable;
std::cout << g_extern; // referenced in extern.cpp
return 0;
}
Notice that g_extern isn’t given the extern keyword. Provided we forward declare g_extern by using the extern keyword, C++ will understand that g_extern is in a separate file and it needs to retrieve it.
Now, let’s say we want to define global variables but keep them in our current source file. For that, we can use the static keyword.
#include <iostream>
#include <string>
// this has 'internal' linkage, and can only be accessed in this source file
static std::string str { "Hello world" };
int main()
{
std::cout << str;
return 0;
}
Attempting to use a static variable in a separate file will result in a very cryptic compilation error:
// extra.cpp
#include <iostream>
#include <string>
extern std::string str; // forward declaration
void print()
{
std::cout << str;
}
// main.cpp
#include <iostream>
#include <string>
void print();
// what happens now?
static std::string str { "Hello world" };
int main()
{
std::cout << str;
print();
return 0;
}
// OUTPUT
undefined reference to `str[abi:cxx11]'
All that we can infer from this error is that somewhere, we have an undefined reference to a string. There’s little chance to mess up like this with the static keyword, but I’m here to show dumb things so you and I don’t do them in the future.
Let’s go over how global variables are the worst thing ever next to the last slice of bread.
One large risk of global variable usage is losing track of their value and/or scope.
Consider the following snippet:
// brought to you by:
// learncpp.com/cpp-tutorial/why-non-const-global-variables-are-evil/
// 🔥
#include <iostream>
int g_mode; // declare global variable (will be zero-initialized by default)
void doSomething()
{
g_mode = 2; // set the global g_mode variable to 2
}
int main()
{
g_mode = 1; // note: this sets the global g_mode variable to 1. It does not declare a local g_mode variable!
doSomething();
// Programmer still expects g_mode to be 1
// But doSomething changed it to 2!
if (g_mode == 1)
{
std::cout << "No threat detected.\n";
}
else
{
std::cout << "Launching nuclear missiles...\n";
}
return 0;
}
This is pretty self-explanatory, but the variable g_mode goes insane in this file with the amount of assignment operations that are happening, which ultimately leads to us launching nuclear missiles. All of this could’ve been null and void if the programmer had been more careful with the variable or simply made it a constant.
Secondly, there’s a HUGE problem with variable initialization. In C++, the order of initialization goes something like this:
- Firstly, there’s static initialization, which initializes literals, constexpr global variables, and non-initialized global variables (these get zero-initialized by default)
- Secondly, there’s dynamic initialization, which is where a lot of things can potentially go wrong.
Consider the following snippet:
// again, thanks to
// learncpp.com/cpp-tutorial/why-non-const-global-variables-are-evil/
int init()
{
return 5;
}
int g_something{ init() }; // non-constexpr initialization
This is a depiction of what non-constexpr global variables go through to become initialized. During this phase, each variable will be initialized by point of definition. While this doesn’t look all that bad, it results in some undefined behaviour if x variable relies on y definition.
// learncpp.com/cpp-tutorial/why-non-const-global-variables-are-evil/
#include <iostream>
int initX(); // forward declaration
int initY(); // forward declaration
int g_x{ initX() }; // g_x is initialized first
int g_y{ initY() };
int initX()
{
return g_y; // g_y isn't initialized when this is called
}
int initY()
{
return 5;
}
int main()
{
std::cout << g_x << ' ' << g_y << '\n';
}
Upon running this seemingly normal file, we get an unexpected result:
// OUTPUT
0 5
All of these weird and undefined cases of global variables mishaps can be fixed up by:
- Using local variables when possible
- Using const/constexpr global variables
- Using helper functions to return variable values from file A to file B
The Heel Meal
Don’t ask what that is, you guys can have fun searching it up online.
Anyway, thanks a bunch for reading guys! I spent the past few days grinding out learncpp to get something of substance to teach ya’ll. I did some things with bit manipulation, but it was rather boring, so I ended up settling for linkage, which I had read over this morning.
Hopefully, I can get another blog out to you guys by the end of the week. If not, then it’ll 100% be out by early of next week or over the weekend.
I’m gonna go eat now. Have a good rest of your day, Medium!
If you enjoyed this blog post, consider following me or subscribing to support me!