convert-c-cpp
Convert C to C++
Convert C code to idiomatic modern C++. This skill extends meta-convert-dev with C-to-C++ specific type mappings, idiom translations, and tooling guidance.
This Skill Extends
meta-convert-dev- Foundational conversion patterns (APTV workflow, testing strategies)
For general concepts like the Analyze → Plan → Transform → Validate workflow, testing strategies, and common pitfalls, see the meta-skill first.
This Skill Adds
- Type mappings: C types → C++ types (primitives, structs, function pointers)
- Idiom translations: C patterns → idiomatic modern C++ (C++11/14/17/20)
- Error handling: Error codes → Exceptions,
std::optional,std::expected - Memory management:
malloc/free→ RAII, smart pointers - Module system: Header guards → Namespaces, modules (C++20)
- Metaprogramming: Preprocessor macros → Templates,
constexpr - Build systems: Makefile → CMake
- Testing: Custom test frameworks → Google Test, Catch2
This Skill Does NOT Cover
- General conversion methodology - see
meta-convert-dev - C language fundamentals - see
lang-c-dev - C++ language fundamentals - see
lang-cpp-dev - Reverse conversion (C++ → C) - see
convert-cpp-c
Quick Reference
| C | C++ | Notes |
|---|---|---|
int* ptr = malloc(n * sizeof(int)) |
std::vector<int> vec(n) |
RAII, no manual free |
struct Point p |
Point p |
No struct keyword needed |
typedef struct { ... } Name; |
struct Name { ... }; |
Implicit typedef |
void* generic |
template<typename T> |
Type-safe generics |
FILE* f = fopen(...) |
std::ifstream f(...) |
RAII file handling |
enum { A, B, C } |
enum class Status { A, B, C } |
Scoped enums |
| Error codes | std::optional, std::expected, exceptions |
Modern error handling |
| Function pointers | std::function, lambdas |
Type-safe callbacks |
NULL |
nullptr |
Type-safe null pointer |
const char* strings |
std::string, std::string_view |
Automatic memory mgmt |
When Converting Code
- Analyze source thoroughly - Identify memory ownership patterns in C code
- Map types first - C primitives → C++ equivalents, structs → classes
- Replace manual memory management -
malloc/free→ RAII, smart pointers - Adopt C++ idioms - Don't write "C code with
cout"; use modern C++ patterns - Use standard library - Replace custom implementations with STL containers/algorithms
- Test incrementally - Convert module by module, ensuring tests pass
- Enable compiler warnings - Use
-Wall -Wextra -Wpedanticto catch issues
Type System Mapping
Primitive Types
| C | C++ | Notes |
|---|---|---|
char, int, long |
Same | C++ inherits C primitive types |
unsigned int |
Same or size_t |
Prefer size_t for sizes/indices |
int8_t, uint8_t |
Same (<cstdint>) |
Exact-width integers |
NULL |
nullptr |
Type-safe null pointer constant |
void* |
Avoid | Use templates or std::any instead |
bool (C99) |
bool |
Native in C++, requires <stdbool.h> in C |
Collection Types
| C | C++ | Notes |
|---|---|---|
int arr[10] |
std::array<int, 10> |
Fixed-size, bounds-checked |
int* arr = malloc(...) |
std::vector<int> |
Dynamic, RAII, automatic resize |
| Linked list (manual) | std::list<T>, std::forward_list<T> |
Standard library |
| Hash table (manual) | std::unordered_map<K, V> |
Efficient lookup |
| Binary tree (manual) | std::map<K, V>, std::set<T> |
Ordered containers |
Composite Types
| C | C++ | Notes |
|---|---|---|
struct Point { int x, y; }; |
struct Point { int x, y; }; |
Same, but struct keyword optional for instances |
typedef struct { ... } Name; |
struct Name { ... }; |
Implicit typedef in C++ |
union Data { ... } |
std::variant<...> |
Type-safe tagged union |
enum { A, B, C } |
enum class Status { A, B, C } |
Scoped, strongly-typed |
| Tagged union (manual) | std::variant<...> |
Type-safe alternative |
Function Types
| C | C++ | Notes |
|---|---|---|
int (*func_ptr)(int, int) |
std::function<int(int, int)> |
Type-erased, can hold lambdas |
typedef int (*Callback)(void*) |
std::function<int(void*)> |
Modern function objects |
void qsort(void*, size_t, ...) |
std::sort(begin, end, comparator) |
Type-safe, no void* |
Idiom Translation
Pattern 1: Memory Management (malloc/free → RAII)
C:
#include <stdlib.h>
int* create_array(size_t n) {
int* arr = malloc(n * sizeof(int));
if (arr == NULL) {
return NULL;
}
for (size_t i = 0; i < n; i++) {
arr[i] = i * 2;
}
return arr;
}
void process() {
int* data = create_array(100);
if (data == NULL) {
return; // Error handling
}
// Use data...
free(data); // Manual cleanup
}
C++:
#include <vector>
std::vector<int> create_array(size_t n) {
std::vector<int> arr(n);
for (size_t i = 0; i < n; i++) {
arr[i] = i * 2;
}
return arr; // Move semantics, no copy
}
void process() {
auto data = create_array(100);
// Use data...
// Automatically freed when data goes out of scope
}
Why this translation:
std::vectormanages memory automatically (RAII)- No risk of memory leaks or use-after-free
- Return by value is efficient with move semantics
- Bounds checking available with
.at(i)instead of[]
Pattern 2: Strings (char* → std::string)
C:
#include <string.h>
#include <stdlib.h>
char* concat_strings(const char* a, const char* b) {
size_t len_a = strlen(a);
size_t len_b = strlen(b);
char* result = malloc(len_a + len_b + 1);
if (result == NULL) {
return NULL;
}
strcpy(result, a);
strcat(result, b);
return result;
}
void example() {
char* str = concat_strings("Hello", "World");
if (str != NULL) {
printf("%s\n", str);
free(str);
}
}
C++:
#include <string>
#include <iostream>
std::string concat_strings(const std::string& a, const std::string& b) {
return a + b; // Operator overloading
}
// Or simply:
std::string concat_strings(const std::string& a, const std::string& b) {
return a + b;
}
void example() {
std::string str = concat_strings("Hello", "World");
std::cout << str << '\n';
// Automatic cleanup
}
Why this translation:
std::stringmanages memory automatically- No buffer overflow risks
- Rich API for string manipulation
- Concatenation via
operator+ - Efficient move semantics for returns
Pattern 3: Error Handling (Error Codes → Exceptions/Optional)
C:
#define SUCCESS 0
#define ERROR_NULL_PTR -1
#define ERROR_NOT_FOUND -2
int find_user(int id, User* out_user) {
if (out_user == NULL) {
return ERROR_NULL_PTR;
}
User* user = lookup_user(id);
if (user == NULL) {
return ERROR_NOT_FOUND;
}
*out_user = *user;
return SUCCESS;
}
// Usage
void example() {
User user;
int result = find_user(42, &user);
if (result == SUCCESS) {
// Use user
} else if (result == ERROR_NOT_FOUND) {
// Handle not found
}
}
C++ (with std::optional):
#include <optional>
std::optional<User> find_user(int id) {
User* user = lookup_user(id);
if (user == nullptr) {
return std::nullopt;
}
return *user;
}
// Usage
void example() {
if (auto user = find_user(42)) {
// Use *user
} else {
// Handle not found
}
}
C++ (with exceptions):
#include <stdexcept>
User find_user(int id) {
User* user = lookup_user(id);
if (user == nullptr) {
throw std::runtime_error("User not found");
}
return *user;
}
// Usage
void example() {
try {
User user = find_user(42);
// Use user
} catch (const std::runtime_error& e) {
// Handle error
}
}
Why this translation:
std::optionalavoids sentinel values and output parameters- Exceptions separate happy path from error handling
- Clearer intent and less error-prone
- Modern C++23 will have
std::expected<T, E>for richer error info
Pattern 4: File I/O (FILE* → RAII streams)
C:
#include <stdio.h>
int read_file(const char* filename, char** out_buffer, size_t* out_size) {
FILE* file = fopen(filename, "r");
if (file == NULL) {
return -1;
}
fseek(file, 0, SEEK_END);
long size = ftell(file);
fseek(file, 0, SEEK_SET);
char* buffer = malloc(size + 1);
if (buffer == NULL) {
fclose(file);
return -1;
}
fread(buffer, 1, size, file);
buffer[size] = '\0';
fclose(file);
*out_buffer = buffer;
*out_size = size;
return 0;
}
C++:
#include <fstream>
#include <sstream>
#include <string>
#include <optional>
std::optional<std::string> read_file(const std::string& filename) {
std::ifstream file(filename);
if (!file) {
return std::nullopt;
}
std::stringstream buffer;
buffer << file.rdbuf();
return buffer.str();
// File automatically closed by destructor
}
Why this translation:
std::ifstreamuses RAII: automatically closes file- No manual memory management for buffer
- Exception-safe: file closed even if exception thrown
- More concise and less error-prone
Pattern 5: Structs → Classes with Methods
C:
typedef struct {
double x;
double y;
} Point;
Point point_create(double x, double y) {
Point p = {x, y};
return p;
}
double point_distance(const Point* p1, const Point* p2) {
double dx = p2->x - p1->x;
double dy = p2->y - p1->y;
return sqrt(dx*dx + dy*dy);
}
void point_print(const Point* p) {
printf("Point(%.2f, %.2f)\n", p->x, p->y);
}
C++:
#include <iostream>
#include <cmath>
struct Point {
double x;
double y;
// Constructor
Point(double x, double y) : x(x), y(y) {}
// Member function
double distance(const Point& other) const {
double dx = other.x - x;
double dy = other.y - y;
return std::sqrt(dx*dx + dy*dy);
}
// Operator overload
friend std::ostream& operator<<(std::ostream& os, const Point& p) {
os << "Point(" << p.x << ", " << p.y << ")";
return os;
}
};
// Usage
Point p1(3.0, 4.0);
Point p2(0.0, 0.0);
std::cout << p1 << '\n';
std::cout << "Distance: " << p1.distance(p2) << '\n';
Why this translation:
- Methods are grouped with data (encapsulation)
- Constructors initialize members correctly
- Operator overloading for natural syntax
- Const correctness enforced by compiler
Pattern 6: Function Pointers → Lambdas/std::function
C:
typedef int (*Comparator)(const void*, const void*);
int int_compare(const void* a, const void* b) {
int ia = *(const int*)a;
int ib = *(const int*)b;
return ia - ib;
}
void sort_array(int* arr, size_t n, Comparator cmp) {
qsort(arr, n, sizeof(int), cmp);
}
// Usage
int data[] = {5, 2, 8, 1, 9};
sort_array(data, 5, int_compare);
C++:
#include <algorithm>
#include <vector>
// Type-safe, no void*
void sort_array(std::vector<int>& arr, auto comparator) {
std::sort(arr.begin(), arr.end(), comparator);
}
// Usage with lambda
std::vector<int> data = {5, 2, 8, 1, 9};
std::sort(data.begin(), data.end(), [](int a, int b) {
return a < b;
});
// Or reverse sort
std::sort(data.begin(), data.end(), [](int a, int b) {
return a > b;
});
Why this translation:
- Lambdas are type-safe (no
void*casting) - Can capture local variables
- Inline definition for simple comparisons
std::sortis faster thanqsort(inlined, type-specific)
Pattern 7: Macros → Templates and constexpr
C:
#define MAX(a, b) ((a) > (b) ? (a) : (b))
#define SQUARE(x) ((x) * (x))
#define ARRAY_SIZE(arr) (sizeof(arr) / sizeof((arr)[0]))
// Generic swap
#define SWAP(a, b, type) do { \
type temp = (a); \
(a) = (b); \
(b) = temp; \
} while(0)
C++:
// Type-safe templates
template<typename T>
constexpr T max(T a, T b) {
return (a > b) ? a : b;
}
template<typename T>
constexpr T square(T x) {
return x * x;
}
template<typename T, size_t N>
constexpr size_t array_size(T (&)[N]) {
return N;
}
// Or use C++17 std::size
#include <iterator>
int arr[] = {1, 2, 3, 4, 5};
size_t size = std::size(arr);
// Swap with template
template<typename T>
void swap(T& a, T& b) {
T temp = std::move(a);
a = std::move(b);
b = std::move(temp);
}
// Or just use std::swap
#include <utility>
std::swap(a, b);
Why this translation:
- Templates provide type safety
constexprenables compile-time evaluation- Standard library provides
std::swap,std::max,std::min - Better error messages than macro errors
- Debugger-friendly (macros are invisible after preprocessing)
Pattern 8: Enums → Scoped Enums
C:
enum Color {
COLOR_RED,
COLOR_GREEN,
COLOR_BLUE
};
enum Status {
STATUS_OK,
STATUS_ERROR
};
// Name conflicts possible
int color = COLOR_RED;
C++:
enum class Color {
Red,
Green,
Blue
};
enum class Status {
Ok,
Error
};
// No name conflicts, must scope
Color color = Color::Red;
Status status = Status::Ok;
// Stronger type safety
// Color c = Status::Ok; // Error: type mismatch
Why this translation:
enum classprevents name conflicts (scoped)- No implicit conversion to int
- Stronger type safety
- Explicit scoping improves readability
Error Handling Translation
C Error Model → C++ Error Models
| C Pattern | C++ Pattern | When to Use |
|---|---|---|
Return code (int) |
std::optional<T> |
Simple success/failure, no error details needed |
| Return code + errno | Exceptions | Rare errors, rich error context |
| Return code + output param | std::expected<T, E> (C++23) |
Error details needed, exceptions undesirable |
| NULL return | std::optional<T> |
May or may not find a value |
Error Code → std::optional
C:
#define SUCCESS 0
#define ERROR_NOT_FOUND -1
int get_config_value(const char* key, int* out_value) {
if (key == NULL || out_value == NULL) {
return -1;
}
// Lookup logic
if (/* not found */) {
return ERROR_NOT_FOUND;
}
*out_value = /* found value */;
return SUCCESS;
}
C++:
std::optional<int> get_config_value(const std::string& key) {
// Lookup logic
if (/* not found */) {
return std::nullopt;
}
return /* found value */;
}
// Usage
if (auto value = get_config_value("timeout")) {
std::cout << "Timeout: " << *value << '\n';
} else {
std::cout << "Key not found\n";
}
Error Code → Exceptions
C:
int open_database(const char* path, Database** out_db) {
if (path == NULL || out_db == NULL) {
return ERROR_INVALID_ARG;
}
Database* db = malloc(sizeof(Database));
if (db == NULL) {
return ERROR_OUT_OF_MEMORY;
}
if (/* connection failed */) {
free(db);
return ERROR_CONNECTION_FAILED;
}
*out_db = db;
return SUCCESS;
}
// Caller must check every error
int result = open_database(path, &db);
if (result != SUCCESS) {
// Handle specific errors
}
C++:
#include <stdexcept>
#include <memory>
class Database {
public:
Database(const std::string& path) {
if (/* connection failed */) {
throw std::runtime_error("Failed to connect to database");
}
// Initialize
}
// RAII: destructor closes connection
~Database() {
// Close connection
}
};
// Usage - exceptions propagate automatically
try {
Database db(path);
// Use db
} catch (const std::runtime_error& e) {
std::cerr << "Error: " << e.what() << '\n';
}
Why this translation:
- Exceptions separate error handling from main logic
- RAII ensures cleanup even if exception thrown
- Can't accidentally ignore errors (uncaught exception terminates)
- Error context preserved through exception object
Memory Management Translation
Manual Allocation → RAII and Smart Pointers
C:
typedef struct {
char* data;
size_t size;
} Buffer;
Buffer* buffer_create(size_t size) {
Buffer* buf = malloc(sizeof(Buffer));
if (buf == NULL) {
return NULL;
}
buf->data = malloc(size);
if (buf->data == NULL) {
free(buf);
return NULL;
}
buf->size = size;
return buf;
}
void buffer_destroy(Buffer* buf) {
if (buf != NULL) {
free(buf->data);
free(buf);
}
}
// Usage - easy to forget cleanup
Buffer* buf = buffer_create(1024);
// ... use buf ...
buffer_destroy(buf); // Must remember to call
C++ (RAII):
#include <vector>
class Buffer {
private:
std::vector<char> data;
public:
Buffer(size_t size) : data(size) {}
// Rule of zero - compiler generates correct copy/move/destructor
char& operator[](size_t i) { return data[i]; }
size_t size() const { return data.size(); }
};
// Usage - automatic cleanup
{
Buffer buf(1024);
// ... use buf ...
} // Automatically destroyed
C++ (Smart Pointers for Heap Allocation):
#include <memory>
class LargeObject {
// ... large data ...
};
// Unique ownership
auto obj = std::make_unique<LargeObject>();
// ... use obj ...
// Automatically deleted when obj goes out of scope
// Shared ownership
auto shared = std::make_shared<LargeObject>();
auto copy = shared; // Reference count = 2
// Deleted when last shared_ptr is destroyed
Why this translation:
- No manual memory management needed
- Impossible to forget cleanup
- Exception-safe (cleanup happens even if exception thrown)
- Clear ownership semantics
Concurrency Translation
pthreads → std::thread and Synchronization Primitives
C (pthreads):
#include <pthread.h>
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
int shared_counter = 0;
void* thread_function(void* arg) {
for (int i = 0; i < 1000; i++) {
pthread_mutex_lock(&mutex);
shared_counter++;
pthread_mutex_unlock(&mutex);
}
return NULL;
}
int main() {
pthread_t thread1, thread2;
pthread_create(&thread1, NULL, thread_function, NULL);
pthread_create(&thread2, NULL, thread_function, NULL);
pthread_join(thread1, NULL);
pthread_join(thread2, NULL);
pthread_mutex_destroy(&mutex);
printf("Counter: %d\n", shared_counter);
return 0;
}
C++:
#include <thread>
#include <mutex>
#include <iostream>
std::mutex mutex;
int shared_counter = 0;
void thread_function() {
for (int i = 0; i < 1000; i++) {
std::lock_guard<std::mutex> lock(mutex); // RAII lock
shared_counter++;
// Automatically unlocked when lock goes out of scope
}
}
int main() {
std::thread thread1(thread_function);
std::thread thread2(thread_function);
thread1.join();
thread2.join();
std::cout << "Counter: " << shared_counter << '\n';
return 0;
}
Why this translation:
std::lock_guardprovides RAII for mutex (can't forget to unlock)std::threadis type-safe (novoid*casting)- Cleaner syntax
- Exception-safe locking
Build System Translation
Makefile → CMake
C (Makefile):
CC = gcc
CFLAGS = -Wall -Wextra -O2 -std=c11
LDFLAGS = -lm
SRCS = main.c utils.c
OBJS = $(SRCS:.c=.o)
TARGET = myapp
all: $(TARGET)
$(TARGET): $(OBJS)
$(CC) $(OBJS) -o $(TARGET) $(LDFLAGS)
%.o: %.c
$(CC) $(CFLAGS) -c $< -o $@
clean:
rm -f $(OBJS) $(TARGET)
C++ (CMake):
cmake_minimum_required(VERSION 3.20)
project(MyApp VERSION 1.0.0 LANGUAGES CXX)
# Set C++ standard
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
# Create executable
add_executable(myapp
src/main.cpp
src/utils.cpp
)
# Include directories
target_include_directories(myapp PRIVATE include)
# Compiler flags
target_compile_options(myapp PRIVATE
-Wall -Wextra -Wpedantic
)
# Link libraries
target_link_libraries(myapp PRIVATE
# Add libraries here
)
Build commands:
# Configure
cmake -B build -S . -DCMAKE_BUILD_TYPE=Release
# Build
cmake --build build
# Clean
cmake --build build --target clean
Why this translation:
- CMake is cross-platform (Windows, Linux, macOS)
- Handles dependencies automatically
- Integrates with package managers (Conan, vcpkg)
- Better IDE support
Testing Translation
Custom Test Framework → Google Test
C (Custom Framework):
#include <assert.h>
#include <stdio.h>
void test_addition() {
assert(add(2, 3) == 5);
assert(add(-1, 1) == 0);
printf("test_addition passed\n");
}
void test_subtraction() {
assert(subtract(5, 3) == 2);
printf("test_subtraction passed\n");
}
int main() {
test_addition();
test_subtraction();
printf("All tests passed\n");
return 0;
}
C++ (Google Test):
#include <gtest/gtest.h>
#include "math.hpp"
TEST(MathTest, Addition) {
EXPECT_EQ(add(2, 3), 5);
EXPECT_EQ(add(-1, 1), 0);
}
TEST(MathTest, Subtraction) {
EXPECT_EQ(subtract(5, 3), 2);
}
// Fixture for common setup
class CalculatorTest : public ::testing::Test {
protected:
void SetUp() override {
calc = std::make_unique<Calculator>();
}
std::unique_ptr<Calculator> calc;
};
TEST_F(CalculatorTest, Operations) {
calc->add(5);
EXPECT_EQ(calc->result(), 5);
}
int main(int argc, char** argv) {
::testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
}
Why this translation:
- Rich assertion API
- Fixtures for setup/teardown
- Test discovery and filtering
- Better error messages
- Integration with CI/CD systems
Serialization Translation
Binary/JSON (C) → C++ Libraries
C (cJSON):
#include <cJSON.h>
char* user_to_json(const User* user) {
cJSON* root = cJSON_CreateObject();
cJSON_AddStringToObject(root, "name", user->name);
cJSON_AddNumberToObject(root, "age", user->age);
char* json_str = cJSON_Print(root);
cJSON_Delete(root);
return json_str;
}
int user_from_json(User* user, const char* json_str) {
cJSON* root = cJSON_Parse(json_str);
if (root == NULL) {
return -1;
}
cJSON* name = cJSON_GetObjectItem(root, "name");
cJSON* age = cJSON_GetObjectItem(root, "age");
if (!cJSON_IsString(name) || !cJSON_IsNumber(age)) {
cJSON_Delete(root);
return -1;
}
strncpy(user->name, name->valuestring, sizeof(user->name) - 1);
user->age = age->valueint;
cJSON_Delete(root);
return 0;
}
C++ (nlohmann/json):
#include <nlohmann/json.hpp>
using json = nlohmann::json;
struct User {
std::string name;
int age;
};
// Serialization
void to_json(json& j, const User& u) {
j = json{{"name", u.name}, {"age", u.age}};
}
// Deserialization
void from_json(const json& j, User& u) {
j.at("name").get_to(u.name);
j.at("age").get_to(u.age);
}
// Usage
User user{"Alice", 30};
json j = user; // Serialize
std::string json_str = j.dump();
// Deserialize
json j2 = json::parse(json_str);
User user2 = j2.get<User>();
Why this translation:
- Type-safe serialization/deserialization
- Automatic memory management
- Exception-based error handling
- Integration with modern C++ types
Module System Translation
Header Guards → Namespaces and C++20 Modules
C (Header Guards):
// point.h
#ifndef POINT_H
#define POINT_H
typedef struct {
double x;
double y;
} Point;
Point point_create(double x, double y);
double point_distance(const Point* p1, const Point* p2);
#endif // POINT_H
C++ (Namespaces):
// point.hpp
#pragma once // Modern alternative to include guards
namespace geometry {
class Point {
public:
Point(double x, double y);
double distance(const Point& other) const;
private:
double x, y;
};
} // namespace geometry
C++20 (Modules):
// point.cppm
export module geometry;
export namespace geometry {
class Point {
public:
Point(double x, double y);
double distance(const Point& other) const;
private:
double x, y;
};
} // namespace geometry
// main.cpp
import geometry;
int main() {
geometry::Point p1(3.0, 4.0);
geometry::Point p2(0.0, 0.0);
auto dist = p1.distance(p2);
}
Why this translation:
- Namespaces prevent name collisions
- Modules (C++20) eliminate header file re-parsing
- Faster compilation with modules
- Better encapsulation
Common Pitfalls
1. Forgetting to Use nullptr Instead of NULL
C:
int* ptr = NULL;
if (ptr == NULL) { /* ... */ }
Wrong C++:
int* ptr = NULL; // Don't use NULL
Correct C++:
int* ptr = nullptr; // Type-safe
if (ptr == nullptr) { /* ... */ }
Why: nullptr is type-safe and works correctly with overloading.
2. Not Using RAII for Resource Management
Wrong:
void process() {
int* data = new int[100];
// ... use data ...
delete[] data; // Easy to forget or skip on early return
}
Correct:
void process() {
std::vector<int> data(100);
// ... use data ...
// Automatically cleaned up
}
3. Casting to void* When Templates Would Work
Wrong:
void* generic_max(void* a, void* b, size_t size, int (*cmp)(const void*, const void*)) {
return cmp(a, b) > 0 ? a : b;
}
Correct:
template<typename T>
T max(T a, T b) {
return (a > b) ? a : b;
}
4. Using C-Style Casts
Wrong:
double d = 3.14;
int i = (int)d; // C-style cast
Correct:
double d = 3.14;
int i = static_cast<int>(d); // Explicit and searchable
5. Manual Memory Management Instead of Smart Pointers
Wrong:
Widget* widget = new Widget();
// ... use widget ...
delete widget; // Might leak on exception
Correct:
auto widget = std::make_unique<Widget>();
// ... use widget ...
// Automatically deleted
6. Using char* for Strings
Wrong:
char* name = new char[50];
strcpy(name, "Alice");
// ... easy to cause buffer overflow ...
delete[] name;
Correct:
std::string name = "Alice";
name += " Smith"; // Safe concatenation
// Automatic cleanup
7. Not Leveraging the Standard Library
Wrong:
// Implement custom linked list, hash table, etc.
Correct:
std::list<int> mylist;
std::unordered_map<std::string, int> mymap;
// Use tested, optimized implementations
8. Ignoring Const Correctness
Wrong:
void print_point(Point* p) { // Should be const
std::cout << p->x << ", " << p->y << '\n';
}
Correct:
void print_point(const Point& p) { // Pass by const reference
std::cout << p.x << ", " << p.y << '\n';
}
Tooling
| Tool | Purpose | Notes |
|---|---|---|
c2rust |
Automated C → Rust translation | Not C++, but useful reference |
clang-tidy |
Static analysis, modernization | Checks for C-isms in C++ code |
cppcheck |
Static analysis | Finds bugs and style issues |
clang-format |
Code formatting | Enforces consistent style |
include-what-you-use |
Header hygiene | Ensures correct includes |
| CMake | Build system | Cross-platform builds |
| Conan / vcpkg | Package managers | Dependency management |
Clang-Tidy Modernization Checks
# Run modernization checks
clang-tidy --checks='modernize-*' src/main.cpp -- -std=c++20
# Example checks:
# - modernize-use-nullptr (NULL → nullptr)
# - modernize-use-auto (explicit type → auto)
# - modernize-use-override (virtual → override)
# - modernize-make-unique (new → make_unique)
# - modernize-raw-string-literal (escape sequences → raw strings)
Examples
Example 1: Simple - Integer Array
Before (C):
#include <stdlib.h>
#include <stdio.h>
int* create_sequence(int n) {
int* arr = malloc(n * sizeof(int));
if (arr == NULL) {
return NULL;
}
for (int i = 0; i < n; i++) {
arr[i] = i + 1;
}
return arr;
}
int main() {
int* seq = create_sequence(10);
if (seq == NULL) {
fprintf(stderr, "Allocation failed\n");
return 1;
}
for (int i = 0; i < 10; i++) {
printf("%d ", seq[i]);
}
printf("\n");
free(seq);
return 0;
}
After (C++):
#include <vector>
#include <iostream>
std::vector<int> create_sequence(int n) {
std::vector<int> arr(n);
for (int i = 0; i < n; i++) {
arr[i] = i + 1;
}
return arr; // Move semantics, efficient
}
int main() {
auto seq = create_sequence(10);
for (int value : seq) {
std::cout << value << ' ';
}
std::cout << '\n';
// Automatic cleanup
return 0;
}
Example 2: Medium - Linked List
Before (C):
#include <stdlib.h>
#include <stdio.h>
typedef struct Node {
int data;
struct Node* next;
} Node;
typedef struct {
Node* head;
} LinkedList;
void list_init(LinkedList* list) {
list->head = NULL;
}
void list_push(LinkedList* list, int value) {
Node* new_node = malloc(sizeof(Node));
if (new_node == NULL) {
return;
}
new_node->data = value;
new_node->next = list->head;
list->head = new_node;
}
void list_free(LinkedList* list) {
Node* current = list->head;
while (current != NULL) {
Node* next = current->next;
free(current);
current = next;
}
list->head = NULL;
}
void list_print(const LinkedList* list) {
Node* current = list->head;
while (current != NULL) {
printf("%d ", current->data);
current = current->next;
}
printf("\n");
}
int main() {
LinkedList list;
list_init(&list);
list_push(&list, 3);
list_push(&list, 2);
list_push(&list, 1);
list_print(&list);
list_free(&list);
return 0;
}
After (C++):
#include <list>
#include <iostream>
int main() {
std::list<int> list;
list.push_front(3);
list.push_front(2);
list.push_front(1);
for (int value : list) {
std::cout << value << ' ';
}
std::cout << '\n';
// Automatic cleanup
return 0;
}
Example 3: Complex - Configuration Parser
Before (C):
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <cJSON.h>
typedef struct {
char* host;
int port;
int timeout;
} Config;
Config* config_create() {
Config* cfg = malloc(sizeof(Config));
if (cfg == NULL) {
return NULL;
}
cfg->host = NULL;
cfg->port = 8080;
cfg->timeout = 30;
return cfg;
}
void config_destroy(Config* cfg) {
if (cfg != NULL) {
free(cfg->host);
free(cfg);
}
}
int config_load(Config* cfg, const char* filename) {
FILE* file = fopen(filename, "r");
if (file == NULL) {
return -1;
}
fseek(file, 0, SEEK_END);
long size = ftell(file);
fseek(file, 0, SEEK_SET);
char* buffer = malloc(size + 1);
if (buffer == NULL) {
fclose(file);
return -1;
}
fread(buffer, 1, size, file);
buffer[size] = '\0';
fclose(file);
cJSON* root = cJSON_Parse(buffer);
free(buffer);
if (root == NULL) {
return -1;
}
cJSON* host = cJSON_GetObjectItem(root, "host");
if (cJSON_IsString(host)) {
cfg->host = strdup(host->valuestring);
}
cJSON* port = cJSON_GetObjectItem(root, "port");
if (cJSON_IsNumber(port)) {
cfg->port = port->valueint;
}
cJSON* timeout = cJSON_GetObjectItem(root, "timeout");
if (cJSON_IsNumber(timeout)) {
cfg->timeout = timeout->valueint;
}
cJSON_Delete(root);
return 0;
}
int main() {
Config* cfg = config_create();
if (cfg == NULL) {
fprintf(stderr, "Failed to create config\n");
return 1;
}
if (config_load(cfg, "config.json") != 0) {
fprintf(stderr, "Failed to load config\n");
config_destroy(cfg);
return 1;
}
printf("Host: %s\n", cfg->host);
printf("Port: %d\n", cfg->port);
printf("Timeout: %d\n", cfg->timeout);
config_destroy(cfg);
return 0;
}
After (C++):
#include <fstream>
#include <iostream>
#include <nlohmann/json.hpp>
#include <optional>
#include <string>
using json = nlohmann::json;
struct Config {
std::string host = "localhost";
int port = 8080;
int timeout = 30;
};
// JSON serialization/deserialization
void from_json(const json& j, Config& cfg) {
j.at("host").get_to(cfg.host);
j.at("port").get_to(cfg.port);
j.at("timeout").get_to(cfg.timeout);
}
std::optional<Config> load_config(const std::string& filename) {
std::ifstream file(filename);
if (!file) {
return std::nullopt;
}
try {
json j;
file >> j;
return j.get<Config>();
} catch (const json::exception& e) {
std::cerr << "JSON error: " << e.what() << '\n';
return std::nullopt;
}
}
int main() {
if (auto cfg = load_config("config.json")) {
std::cout << "Host: " << cfg->host << '\n';
std::cout << "Port: " << cfg->port << '\n';
std::cout << "Timeout: " << cfg->timeout << '\n';
} else {
std::cerr << "Failed to load config\n";
return 1;
}
return 0;
}
Key improvements:
- RAII: file and JSON object automatically cleaned up
std::optionalfor error handling (no output parameters)- Exceptions handle JSON parsing errors
- Type-safe deserialization
- Default member initializers for Config
- No manual memory management
Limitations
None. Both lang-c-dev and lang-cpp-dev have complete 8/8 pillar coverage, providing comprehensive guidance for all aspects of the conversion.
See Also
For more examples and patterns, see:
meta-convert-dev- Foundational patterns with cross-language exampleslang-c-dev- C development patternslang-cpp-dev- C++ development patternslang-cpp-patterns-dev- Advanced C++ design patternslang-cpp-cmake-dev- CMake build configurationpatterns-concurrency-dev- Threads, async, synchronizationpatterns-serialization-dev- JSON, validation, data formatspatterns-metaprogramming-dev- Templates, reflection, code generation