Claude-skill-registry convert-c-cpp
Convert C code to idiomatic C++. Use when migrating C projects to C++, translating C patterns to modern C++ idioms, or refactoring C codebases into C++. Extends meta-convert-dev with C-to-C++ specific patterns covering all 8 pillars (Module, Error, Concurrency, Metaprogramming, Zero/Default, Serialization, Build, Testing).
git clone https://github.com/majiayu000/claude-skill-registry
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/convert-c-cpp" ~/.claude/skills/majiayu000-claude-skill-registry-convert-c-cpp && rm -rf "$T"
skills/data/convert-c-cpp/SKILL.mdConvert 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
- Foundational conversion patterns (APTV workflow, testing strategies)meta-convert-dev
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::optionalstd::expected - Memory management:
/malloc
→ RAII, smart pointersfree - 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 |
|---|---|---|
| | RAII, no manual free |
| | No keyword needed |
| | Implicit typedef |
| | Type-safe generics |
| | RAII file handling |
| | Scoped enums |
| Error codes | , , exceptions | Modern error handling |
| Function pointers | , lambdas | Type-safe callbacks |
| | Type-safe null pointer |
strings | , | 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
→ RAII, smart pointersfree - Adopt C++ idioms - Don't write "C code with
"; use modern C++ patternscout - Use standard library - Replace custom implementations with STL containers/algorithms
- Test incrementally - Convert module by module, ensuring tests pass
- Enable compiler warnings - Use
to catch issues-Wall -Wextra -Wpedantic
Type System Mapping
Primitive Types
| C | C++ | Notes |
|---|---|---|
, , | Same | C++ inherits C primitive types |
| Same or | Prefer for sizes/indices |
, | Same () | Exact-width integers |
| | Type-safe null pointer constant |
| Avoid | Use templates or instead |
(C99) | | Native in C++, requires in C |
Collection Types
| C | C++ | Notes |
|---|---|---|
| | Fixed-size, bounds-checked |
| | Dynamic, RAII, automatic resize |
| Linked list (manual) | , | Standard library |
| Hash table (manual) | | Efficient lookup |
| Binary tree (manual) | , | Ordered containers |
Composite Types
| C | C++ | Notes |
|---|---|---|
| | Same, but keyword optional for instances |
| | Implicit typedef in C++ |
| | Type-safe tagged union |
| | Scoped, strongly-typed |
| Tagged union (manual) | | Type-safe alternative |
Function Types
| C | C++ | Notes |
|---|---|---|
| | Type-erased, can hold lambdas |
| | Modern function objects |
| | 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:
manages memory automatically (RAII)std::vector- No risk of memory leaks or use-after-free
- Return by value is efficient with move semantics
- Bounds checking available with
instead of.at(i)[]
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:
manages memory automaticallystd::string- 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:
avoids sentinel values and output parametersstd::optional- Exceptions separate happy path from error handling
- Clearer intent and less error-prone
- Modern C++23 will have
for richer error infostd::expected<T, E>
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:
uses RAII: automatically closes filestd::ifstream- 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
casting)void* - Can capture local variables
- Inline definition for simple comparisons
is faster thanstd::sort
(inlined, type-specific)qsort
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
enables compile-time evaluationconstexpr- Standard library provides
,std::swap
,std::maxstd::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:
prevents name conflicts (scoped)enum class- 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 () | | Simple success/failure, no error details needed |
| Return code + errno | Exceptions | Rare errors, rich error context |
| Return code + output param | (C++23) | Error details needed, exceptions undesirable |
| NULL return | | 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:
provides RAII for mutex (can't forget to unlock)std::lock_guard
is type-safe (nostd::thread
casting)void*- 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
nullptrNULLC:
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
void*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
char*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 |
|---|---|---|
| Automated C → Rust translation | Not C++, but useful reference |
| Static analysis, modernization | Checks for C-isms in C++ code |
| Static analysis | Finds bugs and style issues |
| Code formatting | Enforces consistent style |
| 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
for error handling (no output parameters)std::optional- 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:
- Foundational patterns with cross-language examplesmeta-convert-dev
- C development patternslang-c-dev
- C++ development patternslang-cpp-dev
- Advanced C++ design patternslang-cpp-patterns-dev
- CMake build configurationlang-cpp-cmake-dev
- Threads, async, synchronizationpatterns-concurrency-dev
- JSON, validation, data formatspatterns-serialization-dev
- Templates, reflection, code generationpatterns-metaprogramming-dev