Reading legacy C++
Code from 1998 still compiles, and most of it looks familiar. The differences are in the small decisions: an array and a separate count instead of a container, a cast that could mean four different conversions, and malloc in a program that also has constructors.
The job with legacy code is not to rewrite it. It is to recognise the idiom, know what the modern replacement is, and change the parts that a bug or a new feature touches. This lesson reads four of those idioms.
The rule for a legacy file is to change what the current task touches and leave the rest. A rewrite is a project of its own, and the mixed file compiles as long as the new code uses the modern type.
An array and its count
A C-style array carries no length, so the old code either hard-codes the count or computes it with sizeof(array) / sizeof(array[0]). The trick works only where the array has not decayed to a pointer.
The program sums a raw array with an index loop and a std::array with a range-for, and prints the count from the sizeof expression. The answers are 10, 10 and 4, and the second version cannot disagree with its own size.
The modern replacement is std::array for a fixed size and std::vector for a growing one, both of lesson 020. A function that receives the raw array still needs the count as a second parameter, which is why the pair appears in every old interface.
#include <array>
#include <iostream>
int main()
{
int raw[4] = {1, 2, 3, 4};
std::array<int, 4> modern{1, 2, 3, 4};
int raw_sum = 0;
for (int i = 0; i < 4; ++i) {
raw_sum += raw[i];
}
int modern_sum = 0;
for (int value : modern) {
modern_sum += value;
}
std::cout << raw_sum << ' ' << modern_sum << ' ';
std::cout << sizeof(raw) / sizeof(raw[0]) << '\n';
}
g++ -std=c++23 -Wall -Wextra arrays.cpp -o arrays
./arrays
10 10 4
The cast that says nothing
(int)value is a C cast, and the compiler picks one of several conversions without saying which. static_cast<int>(value) says that the conversion is a value conversion, and the other three named casts cover the cases a static_cast cannot.
The program truncates a double with both forms and converts an integer to a char. The results are the same, and the named cast is the one that a reader and a compiler warning can check.
The rule for legacy code is to replace a C cast with the narrowest named cast that compiles: static_cast for values, reinterpret_cast for bits, const_cast for qualifiers and dynamic_cast for a checked downcast. A C cast can silently become any of the four.
#include <iostream>
int main()
{
double value = 3.9;
int old_style = (int)value;
int new_style = static_cast<int>(value);
char letter = static_cast<char>(65);
std::cout << old_style << ' ' << new_style << ' ' << letter << '\n';
}
g++ -std=c++23 -Wall -Wextra casts.cpp -o casts
./casts
3 3 A
malloc in a C++ program
malloc returns raw bytes and does not run a constructor, and free does not run a destructor. For a block of chars that is exactly right; for an object of a class it leaves the object in a state no constructor ever produced.
The program allocates six bytes for a string and copies five letters and a terminator into them. The print is hello and 5, and the code is correct because a char array has no constructor to miss.
For a class the replacement is new and delete of lesson 089, or better a std::vector and a std::string that own the block. A legacy buffer of bytes is the one case where malloc survives in C++.
#include <cstdlib>
#include <cstring>
#include <iostream>
int main()
{
char* text = static_cast<char*>(std::malloc(6));
std::strcpy(text, "hello");
std::cout << text << ' ' << std::strlen(text) << '\n';
std::free(text);
}
g++ -std=c++23 -Wall -Wextra malloc.cpp -o malloc_demo
./malloc_demo
hello 5
The void pointer in C++
C converts a void* to any object pointer without a cast, and C++ does not. Code moved from C to C++ stops at the assignment, and the message says that a void pointer cannot be converted to a char pointer.
The program calls malloc and assigns the result to a char*. The compiler reports the invalid conversion and suggests a cast with -fpermissive, which is the flag to read as a warning about the code, not an instruction to use it.
The fix is static_cast<char*> when the block is really bytes, or the C++ container that owns it. The error is one of the few places where the two languages differ in what an expression means, which is the subject of the next lesson.
#include <cstdlib>
int main()
{
char* text = std::malloc(6);
std::free(text);
}
g++ -std=c++23 -Wall -Wextra void.cpp -o void_pointer
void.cpp: In function 'int main()':
void.cpp:5:29: error: invalid conversion from 'void*' to 'char*' [-fpermissive]
5 | char* text = std::malloc(6);
| ~~~~~~~~~~~^~~
| |
| void*
Exercises
- Replace the raw array in the first program with a std::array and delete the sizeof expression.
- Rewrite the casts program with reinterpret_cast for the bits of a float and print them in hexadecimal.
- Find one C cast in a program of your own and replace it with the named cast that compiles.
Questions
A new set of questions in a new order every time. Choose one answer for each, then check.
-
Why does the sizeof count work only in the scope that declares the array?
Answer
Because the array decays to a pointer when it is passed to a function
A parameter of array type is a pointer, and sizeof of a pointer is the pointer size.
-
What is the problem with a C-style cast?
Answer
It can silently be any of several conversions
The reader cannot see whether the cast reinterprets bits or converts a value, and neither can a warning.
-
What does
mallocnot do for an object of a class type?Answer
Run the constructor
malloc returns bytes, and only new runs the constructor of the type.
-
What does this program print?
#include <array> #include <iostream> int main() { int raw[4] = {1, 2, 3, 4}; std::array<int, 4> modern{1, 2, 3, 4}; int raw_sum = 0; for (int i = 0; i < 4; ++i) raw_sum += raw[i]; int modern_sum = 0; for (int value : modern) modern_sum += value; std::cout << raw_sum << ' ' << modern_sum << ' ' << sizeof(raw) / sizeof(raw[0]); }Answer
10 10 4
Both sums are 10, and the sizeof expression gives the element count 4.
-
What does this program print?
#include <iostream> int main() { double value = 3.9; int old_style = (int)value; int new_style = static_cast<int>(value); char letter = static_cast<char>(65); std::cout << old_style << ' ' << new_style << ' ' << letter; }Answer
3 3 A
Both casts truncate 3.9 to 3, and 65 is the letter A.
-
What does this program print?
#include <cstdlib> #include <cstring> #include <iostream> int main() { char* text = static_cast<char*>(std::malloc(6)); std::strcpy(text, "hello"); std::cout << text << ' ' << std::strlen(text); std::free(text); }Answer
hello 5
The copied text is hello and strlen does not count the terminator.
-
What does the compiler say about
char* text = std::malloc(6);in C++?Answer
That a void pointer cannot be converted to a char pointer
C++ has no implicit conversion from void* to another object pointer, unlike C.
-
Which cast should replace
(int)valuefor a double to int conversion?Answer
static_cast<int>(value)The conversion is a value conversion, which is exactly what static_cast names.
-
When is malloc still acceptable in a C++ program?
Answer
For a block of bytes with no constructor, such as a char buffer
A buffer of bytes has no object lifetime to start, so the C allocator is enough.
-
What is the modern replacement for a raw array and a separate count?
Answer
std::arrayorstd::vectorThe containers carry their size, so the count cannot disagree with the data.
The code on this page was built with g++ 15.2.0 and run on Alpine Linux 3.24.3 (x86_64) in a Docker container. The programs whose section shows a compiler error were compiled until that error and were not run.
Article author: Arthur Isaev