In lesson 3.1 -- Syntax and semantic errors, we covered syntax errors
, which occur when you write code that is not valid according to the grammar of the C++ language. The compiler will notify of you these types of errors, so they are trivial to catch, and usually straightforward to fix.
We also covered semantic errors
, which occur when you write code that does not do what you intended. The compiler generally will not catch semantic errors (though in some cases, smart compilers may be able to generate a warning).
Semantic errors can cause most of the same symptoms of undefined behavior
, such as causing the program to produce the wrong results, causing erratic behavior, corrupting program data, causing the program to crash -- or they may not have any impact at all.
When writing programs, it is almost inevitable that you will make semantic errors. You will probably notice some of these just by using the program: for example, if you were writing a maze game, and your character was able to walk through walls. Testing your program (7.12 -- Introduction to testing your code) can also help surface semantic errors.
But there’s one other thing that can help -- and that’s knowing which type of semantic errors are most common, so you can spend a little more time ensuring things are right in those cases.
In this lesson, we’ll cover a bunch of the most common types of semantic errors that occur in C++ (most of which have to do with flow control in some way).
Conditional logic errors
One of the most common types of semantic errors is a conditional logic error. A conditional logic error occurs when the programmer incorrectly codes the logic of a conditional statement or loop condition. Here is a simple example:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
#include <iostream> int main() { std::cout << "Enter an integer: "; int x{}; std::cin >> x; if (x >= 5) // oops, we used operator>= instead of operator> std::cout << x << " is greater than 5"; return 0; } |
Here’s a run of the program that exhibits the conditional logic error:
Enter an integer: 5 5 is greater than 5
When the user enters 5
, the conditional expression x >= 5
evaluates to true
, so the associated statement is executed.
Here’s another example, using a for loop:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
#include <iostream> int main() { std::cout << "Enter an integer: "; int x{}; std::cin >> x; // oops, we used operator> instead of operator< for (unsigned int count{ 1 }; count > x; ++count) { std::cout << count << ' '; } return 0; } |
This program is supposed to print all of the numbers between 1 and the number the user entered. But here’s what it actually does:
Enter an integer: 5
It didn’t print anything. This happens because on entrance to the for loop, count > x
is false
, so the loop never iterates at all.
Infinite loops
In lesson 7.7 -- Intro to loops and while statements, we covered infinite loops, and showed this example:
1 2 3 4 5 6 7 8 9 10 11 12 |
#include <iostream> int main() { int count{ 1 }; while (count <= 10) // this condition will never be false { std::cout << count << ' '; // so this line will repeatedly execute } return 0; // this line will never execute } |
In this case, we forgot to increment count
, so the loop condition will never be false, and the loop will continue to print:
1 1 1 1 1 1 1 1 1 1
until the user shuts down the program.
Here’s another example that teachers love asking as a quiz question. What’s wrong with the following code?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
#include <iostream> int main() { for (unsigned int count{ 5 }; count >= 0; --count) { if (count == 0) std::cout << "blastoff! "; else std::cout << count << ' '; } return 0; } |
This program is supposed to print 5 4 3 2 1 blastoff!
, which it does, but it doesn’t stop there. In actuality, it prints:
5 4 3 2 1 blastoff! 4294967295 4294967294 4294967293 4294967292 4294967291
and then just keeps decrementing. The program will never terminate, because count >= 0
can never be false
when count
is an unsigned integer.
Off-by-one errors
An off-by-one error is an error that occurs when a loop executes one too many or one too few times. Here’s an example that we covered in lesson 7.9 -- For statements:
1 2 3 4 5 6 7 8 9 10 11 |
#include <iostream> int main() { for (unsigned int count{ 1 }; count < 5; ++count) { std::cout << count << ' '; } return 0; } |
This code is supposed to print 1 2 3 4 5
, but it only prints 1 2 3 4
because the wrong relational operator was used.
Incorrect operator precedence
From lesson 5.7 -- Logical operators, the following program makes an operator precedence mistake:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
#include <iostream> int main() { int x{ 5 }; int y{ 7 }; if (!x > y) std::cout << x << " is not greater than " << y << '\n'; else std::cout << x << " is greater than " << y << '\n'; return 0; } |
Because logical NOT
has higher precedence than operator>
, the conditional evaluates as if it were written (!x) > y
, which isn’t what the programmer intended.
As a result, this program prints:
5 is greater than 7
This can also happen when mixing Logical OR and Logical AND in the same expression (Logical AND takes precedent over Logical OR). Use explicit parenthesization to avoid these kinds of errors.
Precision issues with floating point types
The following floating point variable doesn’t have enough precision to store the entire number:
1 2 3 4 5 6 7 |
#include <iostream> int main() { float f{ 0.123456789f }; std::cout << f; } |
As a consequence, this program prints:
0.123457
In lesson 5.6 -- Relational operators and floating point comparisons, we talked about how using operator==
and operator!=
can be problematic with floating point numbers due to small rounding errors (as well as what to do about it). Here’s an example:
1 2 3 4 5 6 7 8 9 10 11 |
#include <iostream> int main() { double d{ 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 }; // should sum to 1.0 if (d == 1.0) std::cout << "equal"; else std::cout << "not equal"; } |
This program prints:
not equal
The more arithmetic you do with a floating point number, the more it will accumulate small rounding errors.
Integer division
In the following example, we mean to do a floating point division, but because both operands are integers, we end up doing an integer division instead:
1 2 3 4 5 6 7 8 9 10 11 |
#include <iostream> int main() { int x{ 5 }; int y{ 3 }; std::cout << x << " divided by " << y << " is: " << x / y; // integer division return 0; } |
This prints:
5 divided by 3 is: 1
In lesson 5.2 -- Arithmetic operators, we showed that we can use static_cast to convert one of the integral operands to a floating point value in order to do floating point division.
Accidental null statements
In lesson 7.3 -- Common if statement problems, we covered null statements
, which are statements that do nothing.
In the below program, we only want to blow up the world if we have the user’s permission:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
#include <iostream> void blowUpWorld() { std::cout << "Kaboom!\n"; } int main() { std::cout << "Should we blow up the world again? (y/n): "; char c{}; std::cin >> c; if (c=='y'); // accidental null statement here blowUpWorld(); // so this will always execute since it's not part of the if-statement return 0; } |
However, because of an accidental null statement
, the function call to blowUpWorld()
is always executed, so we blow up it regardless:
Should we blow up the world again? (y/n): n Kaboom!
Not using a compound statement when one is required
Another variant of the above program that always blows up the world:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
#include <iostream> void blowUpWorld() { std::cout << "Kaboom!\n"; } int main() { std::cout << "Should we blow up the world again? (y/n): "; char c{}; std::cin >> c; if (c=='y') std::cout << "Okay, here we go...\n"; blowUpWorld(); // oops, will always execute. Should be inside compound statement. return 0; } |
This program prints:
Should we blow up the world again? (y/n): n Kaboom!
A dangling else
(covered in lesson 7.3 -- Common if statement problems) also falls into this category.
What else?
The above represents a good sample of the most common type of semantic errors new C++ programmers tend to make, but there are plenty more. Readers, if you have any additional ones that you think are common pitfalls, leave a note in the comments.
![]() |
![]() |
![]() |
The last part of the tutorial has a repeated word:
The above represents a good sample [[of of]] the most common type of semantic.
Thanks!
In x{5}, instead of using static cast, writing x{5.0} will also do the same job. So, any problem if I don't use static cast and write as x{5.0}? Or it doesn't considered as best practice?
The static_cast should be on the use of x, not the definition of x.
I don't know if it's a common mistake but an easy one to make that the complier might not pick up on is:
instead of
which can produce unexpected results while not pointing to the location of the error.
Can be.
'\' is only reserved for programming purpose. Everything else in this world use '/'.
That line should make sense :v
Great lesson, thanks :D
First paragraph:
> The compiler will notify of you these type of errors
"type" should be "types".
Section "Incorrect operator precedence", last sentence:
> Use explicit parenthesization to avoid these kind of errors.
"kind" should be "kinds".
Grammar updated. Thanks!
I have just passed through one, which may be a mix of some.
When increasing or decreasing counters in loops or if statements, be aware of the location where the increment/decrement occurs.
This code was leading me to print "You have 0 lives left.". After correction, it's changed to:
Now I decrease the Lives and compare: if it is higher than 0, the message of how many lives is generated and the new prompt to guess executes.
If Lives == 0, then it's game over.
What about writing if (x=0) instead of if (x==0)? Is that semantic or syntactic? What do you think about the suggestion of writing if (0==x)?
Also it helps to point out that turning on compiler warnings can catch some of the errors.
Using assignment instead of comparison is a semantic error. It will compile fine, but it won't do what you want.
The advantage of
(0==x)
is that if you accidentally use assignment instead, then it won't compile, since you can't assign to 0.The disadvantage is it doesn't map to how we talk about things (we usually say "compare x to 0", not "compare 0 to x").
Personally, I think either is fine.
Small suggestion: Testing your program (7.12 -- Introduction to testing your code) instead of Testing your program (6.11 -- Scope, duration, and linkage summary)
Also fixed. Thanks again!