Why this matters
Every variable you write has a name, and the name is the only way you and the next reader can tell what it holds. A name such as x2 or tmp forces the reader to guess. A name such as student_count explains itself. Some names are not allowed at all, and the compiler stops with an error when it meets one.
A declaration is the line that introduces a variable and gives its type. In C++ the type part can be short, as in int total, or long, as in std::map<std::string, int> scores. A small change in the type, such as where the word const goes, changes what the code is allowed to do. Reading declarations correctly lets you know, before you run the program, what the compiler will accept and what it will refuse.
This lesson covers two things: the rules and habits for naming, and a method for reading a declaration aloud. It does not cover what variables are or how types work. Earlier lessons in this module cover number types and their ranges, and the pointer lessons later in the course cover addresses in full.
A name is a label you choose
A name in C++ is a sequence of characters that you pick to refer to a variable, a constant, a function or a type. Think of a name as a label on a jar. The label must be written in a way the label printer accepts, and the label should say what is inside. The compiler is the printer. It rejects some labels outright.
The rules for a legal name are short:
- It uses only letters, digits and the underscore character
_. - It does not start with a digit.
- It is not a keyword. A keyword is a word that C++ already uses for its own purpose, such as
int,return,ifandclass. - Upper case and lower case letters are different.
totalandTotalare two different names.
The next program declares three variables. Two of them differ only in case, and one has a digit and an underscore. The output shows that all three are separate variables.
#include <iostream>
#include <iterator>
constexpr int max_users = 100;
const double tax_rate = 0.18;
int main() {
int total = 1;
int Total = 2;
int total_2 = 3;
int arr[max_users] = {};
std::cout << "total = " << total << ", Total = " << Total << ", total_2 = " << total_2 << '\n';
std::cout << "array size = " << std::size(arr) << ", tax rate = " << tax_rate << '\n';
return 0;
}Compile and run it:
g++ -std=c++20 -Wall -o naming_ok naming_ok.cpp
./naming_oktotal = 1, Total = 2, total_2 = 3
array size = 100, tax rate = 0.18The name std::size comes from the <iterator> header. It returns the number of elements in an array, so the second line prints 100. The next section explains constexpr and const, which the two constants at the top use.
Some names are reserved. A name that contains two underscores in a row, such as total__sum, is reserved, and so is a name that starts with an underscore followed by a capital letter, such as _Total. The C++ standard reserves these forms for the compiler and the standard library, so your own code should never use them.
Choosing a readable name
The rules above say what the compiler accepts. The following habits say what a person can read.
- Say what the value means, not how it was made.
student_countis better thann, andtax_rateis better thanx. - Keep names short, but not so short that they hide meaning.
iis a fine loop counter in a three-line loop. A name likeifor a value that lives for the whole program is not. - Do not use letters that look like digits.
llooks like1andOlooks like0, so the nameslandOcause mistakes when people read them on screen or on paper. - Use one style everywhere. This lesson's examples write variables and constants in lower case with words joined by underscores, as in
max_users. Many C++ teams write type names in capital-case words, such asStudentList. What matters is that one project uses one style.
Here is a rough comparison of names for the same value, the total of a list of prices:
| Name | Reader's first question |
|---|---|
t | What is t? |
tmp | Temporary what? |
total_price | Total of what? Probably prices, which is clear |
Constants: const and constexpr
A constant is a variable whose value must not change after it is set. C++ has two keywords for this.
const means the value cannot be changed after it is given. The line const double tax_rate = 0.18; makes tax_rate read-only for the rest of its life. Any later attempt to change it is refused by the compiler. The next program tries it, and the error messages show the refusal:
int main() {
const double tax_rate = 0.18;
tax_rate = 0.2;
return 0;
}error: assignment of read-only variable ‘tax_rate’constexpr means the value must be known to the compiler, before the program runs. This matters when the compiler needs a fixed number, for example to decide the size of an array. The array size in int arr[max_users] must be known at compile time, so max_users is declared constexpr. The rule is simple: if the value is a fixed number that you write in the source code, constexpr is the right choice. If the value is only known while the program runs, then it cannot be constexpr.
The following program tries to use a value that is only known at run time as a compile-time constant. The compiler refuses it:
int main() {
int n = 10;
constexpr int size = n;
return 0;
}error: the value of ‘n’ is not usable in a constant expression
note: ‘int n’ is not constThe fix depends on what you want. If size should be fixed, write constexpr int size = 10;. If its value comes from the input, use a plain const int size = n;, which is only fixed after it is set, and the compiler accepts it.
Reading a declaration aloud
A declaration can look like a puzzle, especially when it has *, &, [ ] or parentheses. A method helps. The idea is to start at the name, then add the other parts in a fixed order. In plain words, the name is the subject of the sentence, and the other symbols say what kind of thing it is.
Use these steps:
- Find the name. Put your finger on it.
- Read what is attached directly after the name first.
[3]means "an array of 3".(int, int)means "a function taking two ints". If there is nothing after the name, skip this step. - If the name sits inside parentheses with a
*or&, read that part next. The*means "a pointer to". - Read the words to the left of the name, nearest first, until you reach the base type such as
intorchar.
Each step adds one piece to the sentence. The procedure can look long, but once you have done a few declarations, it takes a few seconds. Start with a simple one.
int total; has nothing after the name and nothing to the left except int. The sentence is "total is an int."
const int* view; is read as follows. The name is view. Nothing comes after it. To its left is *, so the sentence starts "view is a pointer to". The word to the left of * is const int, so the sentence ends "const int". The result is "view is a pointer to a const int." The const belongs to the int, and it means the int cannot be changed through view.
int* const fixed = &value; is read as follows. The name is fixed. To its left is const, which applies to the thing on its left side, so the sentence starts "fixed is a const". The next word to the left is *, so the sentence reads "fixed is a const pointer to". Then comes int. The result is "fixed is a const pointer to an int." Here the pointer cannot change, but the int it points at can.
The two forms look almost the same. The position of const decides which of the two things is constant. Read it from the name outward, and you will not confuse them.
Arrays use the same method. int grid[3][4]; has [3][4] after the name. Read the first bracket first: "grid is an array of 3 elements." Then read the second bracket for each element: "each element is an array of 4 ints." So grid is an array of 3 arrays of 4 ints. Reading left to right also works here, because the brackets come in the same order as the sentence.
Templates use angle brackets, which hold a type inside another type. std::map<std::string, int> scores; is read from the outside in: "scores is a map, whose keys are strings and whose values are ints." The part in brackets is the description of the inside type. You do not need to know how a map works to read the declaration. The name is scores, and the outer type is std::map.
A harder example: a function pointer
A function pointer is a variable that can hold a function, so that the program can call whichever function it holds. The declaration int (*op)(int, int) looks strange at first. Follow the steps.
The name is op. Inside the parentheses that wrap the name, there is *, so the first part is "op is a pointer to". After the closing parentheses comes (int, int), which describes a function taking two ints, so the sentence reads "a function taking two ints". To the left of everything is int, which is the return type, so the sentence ends "returning an int."
The full reading is "op is a pointer to a function taking two ints and returning an int."
Remove the parentheses around *op and the meaning changes. The declaration int *op(int, int); declares a function named op that returns an int pointer. The * now belongs to the return type, not to the name. This is one of the most common reading mistakes, and the parentheses are the only thing that separates the two readings.
Checking your reading with static_assert
Reading a declaration is a skill you can check. The C++ keyword static_assert asks the compiler to check a condition while it compiles the program. If the condition is false, the compiler stops with an error. The next program declares several variables and then states, for each one, the type it should have. The check uses std::is_same_v from the <type_traits> header, which compares two types and reports whether they match.
#include <map>
#include <string>
#include <type_traits>
#include <vector>
int add(int a, int b) { return a + b; }
int main() {
std::vector<std::string> names{"Asha", "Ravi"};
std::map<std::string, int> scores{{"Asha", 90}};
int grid[3][4]{};
const char* label = "total";
int value = 5;
int* const fixed = &value;
const int* view = &value;
int (*op)(int, int) = add;
int* b_ptr, b_int;
static_assert(std::is_same_v<decltype(names), std::vector<std::string>>);
static_assert(std::is_same_v<decltype(scores), std::map<std::string, int>>);
static_assert(std::is_same_v<decltype(grid), int[3][4]>);
static_assert(std::is_same_v<decltype(label), const char*>);
static_assert(std::is_same_v<decltype(fixed), int* const>);
static_assert(std::is_same_v<decltype(view), const int*>);
static_assert(std::is_same_v<decltype(op), int (*)(int, int)>);
static_assert(std::is_same_v<decltype(b_ptr), int*>);
static_assert(std::is_same_v<decltype(b_int), int>);
return 0;
}decltype(name) gives the declared type of a variable. Each check states the type that your reading of the matching line gave. If every check passes, the program compiles, prints nothing and returns 0. Compile it with the command below and check the exit code:
g++ -std=c++20 -Wall -o declarations declarations.cpp
./declarations
echo "exit code $?"exit code 0Every static_assert is a sentence about the code that the compiler verifies. A check that fails is the sign that your reading was wrong.
Common mistakes
A name that starts with a digit. The compiler reads 2x as a number, not as a name, and stops:
int main() {
int 2x = 5;
return 0;
}error: expected unqualified-id before numeric constantRename it to x2 or two_x. A name must start with a letter or an underscore.
A keyword used as a name. The word class is a keyword, so the compiler reads int class as a broken sentence:
int main() {
int class = 3;
return 0;
}error: expected primary-expression before ‘int’Choose another word such as class_size. A quick way to avoid keywords is to use a longer, more specific name. The keyword list is in any C++ reference.
A hyphen in a name. The name my-count is not one name. The hyphen is the minus operator, so the compiler reads my - count:
int main() {
int my-count = 4;
return 0;
}error: expected initializer before ‘-’ tokenWrite my_count or my_count_value. Hyphens are common in file names and in web addresses, but they are not allowed in C++ names.
Thinking int* p, q; makes two pointers. The * belongs to the name next to it, which is p. The name q is a plain int. The following program checks that reading, and it fails:
#include <type_traits>
int main() {
int* b_ptr, b_int;
static_assert(std::is_same_v<decltype(b_int), int*>);
return 0;
}error: static assertion failedTo declare two pointers, write int* p, *q; or two separate lines: int* p; and int* q;.
Changing a const through a pointer. The declaration const int* view says the int cannot be changed through view. Writing through it is refused:
int main() {
int value = 5;
const int* view = &value;
*view = 3;
return 0;
}error: assignment of read-only location ‘* view’The fix is to decide which object you want to protect. If the int should be changeable, use int* view. If the pointer itself should not move, use int* const.
Practice
Exercise 1. Which of these names are legal in C++: total_2, 2total, my-count, return, Total, max_size? Give a reason for each one that is not legal.
Show the answer
Legal: total_2, Total, max_size. Not legal: 2total, because it starts with a digit. my-count, because the hyphen is the minus operator. return, because it is a keyword.
Exercise 2. Read these declarations aloud in plain words: const char* text;, int* const slot;, double prices[5];, std::vector<int> nums;, int (*f)(int);.
Show the answer
textis a pointer to a const char.slotis a const pointer to an int.pricesis an array of 5 doubles.numsis a vector of ints.fis a pointer to a function taking one int and returning an int.
Exercise 3. The line int 1st_place = 1; does not compile. Rename the variable so that it compiles and keeps its meaning.
Show the answer
Rename it to first_place:
int main() {
int first_place = 1;
return first_place - 1;
}The name now starts with a letter. The program compiles with no error, and it returns 0.
Exercise 4. Declare three things: a constant int named max_items with the value 50, an array of 10 double values named temps, and a std::vector of std::string named lines. Use constexpr for the constant, because it is a fixed number.
Show the answer
#include <iterator>
#include <string>
#include <vector>
constexpr int max_items = 50;
int main() {
double temps[10] = {};
std::vector<std::string> lines;
return std::size(temps) == 10 && max_items == 50 ? 0 : 1;
}The constexpr makes max_items a compile-time value. The array temps has 10 slots, all set to 0 by the {}. The vector lines starts empty. The return line checks the size of temps and the value of max_items, and it returns 0 when both are right. Compiled with -Wall, this version produces no warnings.
Exercise 5. In the declaration int* p, q;, is q a pointer? Answer without running the code, then check it with static_assert.
Show the answer
No. q is a plain int, because the * sticks to p. A check with static_assert(std::is_same_v<decltype(q), int>); compiles, and the same check with int* in place of int fails with error: static assertion failed.
Summary
- A legal name uses letters, digits and underscores, does not start with a digit, is not a keyword, and is case sensitive.
- Pick names that say what a value means, and use one style across the whole program.
constmeans the value cannot change after it is set, andconstexprmeans the compiler must know the value.- Read a declaration by starting at the name, then the part directly after it, then the words to its left.
- The
*andconstbelong to the name or the type next to them, soint* p, q;makes onlypa pointer. - Check a declaration you are unsure of with
static_assertanddecltype.