Skillversitas

55. C++ Tips & Tricks

Definition

Tips and tricks in C++ cover various aspects: compiler configuration, debugging, performance optimization, code organization, and best practices. This knowledge separates a competent C++ developer from one who can merely compile code.

Compiler Flags

Essential Flags

# Compile with all warnings enabled
g++ -Wall -Wextra -Werror -o program main.cpp

# With optimization
g++ -Wall -O2 -std=c++17 -o program main.cpp

# Debug build (for gdb)
g++ -Wall -g -std=c++17 -o program main.cpp

# Release build (maximum optimization)
g++ -Wall -O3 -DNDEBUG -std=c++17 -o program main.cpp

Flag Explanation

Flag Function
-Wall Enable all common warnings
-Wextra Enable additional warnings
-Werror Treat warnings as errors
-O2 Optimization level 2 (recommended)
-O3 Optimization level 3 (more aggressive)
-g Include debug info for gdb
-std=c++17 Use the C++17 standard
-DNDEBUG Disable assert()
-fsanitize=address Memory sanitizer
-fsanitize=undefined Undefined behavior sanitizer

Debugging with gdb

# Compile with debug info
g++ -g -std=c++17 -o program main.cpp

# Run gdb
gdb ./program

Basic gdb Commands

# Inside gdb:
run                 # run the program
break main          # breakpoint in the main function
break file.cpp:42   # breakpoint at line 42
continue            # continue execution
next                # execute the next line (step over)
step                # step into a function (step into)
print myVar      # print a variable's value
backtrace           # show the call stack
info locals         # show all local variables
watch myVar      # watch a variable for changes
quit                # exit gdb

Debugging Example

// main.cpp
#include <iostream>
#include <vector>
using namespace std;

int findMax(const vector<int>& data) {
    int max = data[0];
    for (size_t i = 1; i < data.size(); i++) {
        if (data[i] > max) {
            max = data[i];
        }
    }
    return max;
}

int main() {
    vector<int> numbers = {10, 25, 30, 15, 40};
    int max = findMax(numbers);
    cout << "Max: " << max << endl;
    return 0;
}
# Compile
g++ -g -std=c++17 -o program main.cpp

# Debug
gdb ./program
(gdb) break findMax
(gdb) run
(gdb) print i
(gdb) print data[i]
(gdb) next
(gdb) continue

Memory Debugging with Valgrind

# Install valgrind (macOS/Linux)
# brew install valgrind  (macOS)
# sudo apt install valgrind  (Linux)

# Run valgrind
valgrind --leak-check=full --show-leak-kinds=all ./program

Memory Leak Example

#include <iostream>
using namespace std;

void memoryLeak() {
    int* data = new int[100]; // never deleted!
    data[0] = 42;
    // memory leak is here
}

int main() {
    memoryLeak();
    return 0;
}

Valgrind Output

==12345== HEAP SUMMARY:
==12345==     in use at exit: 400 bytes in 1 blocks
==12345==   total heap usage: 1 allocs, 0 frees, 400 bytes allocated
==12345==
==12345== 400 bytes in 1 blocks are definitely lost
==12345==    at 0x...: operator new[] (vg_replace_malloc.c:...)
==12345==    by 0x...: memoryLeak() (main.cpp:4)
==12345==    by 0x...: main (main.cpp:8)

Tips to Avoid Memory Leaks

#include <iostream>
#include <memory>
#include <vector>
using namespace std;

// BAD — raw pointer, raw new
void bad() {
    int* p = new int(42);
    // forgot to delete p;
}

// GOOD — smart pointer
void good() {
    auto p = make_unique<int>(42);
    // automatically deleted when the scope ends
}

// GOOD — stack allocation
void better() {
    int p = 42;
    // no delete needed
}

// GOOD — container
void best() {
    vector<int> data(100);
    // automatically managed
}

int main() {
    good();
    better();
    best();
    return 0;
}

Code Organization

Directory Structure

project/
├── include/
│   ├── MyClass.h
│   └── utils.h
├── src/
│   ├── main.cpp
│   ├── MyClass.cpp
│   └── utils.cpp
├── tests/
│   └── test_main.cpp
├── CMakeLists.txt
└── README.md

Header Files vs Source Files

// ========== MyClass.h (header) ==========
#ifndef MYCLASS_H
#define MYCLASS_H

#include <string>
#include <vector>

class MyClass {
private:
    std::string name;
    std::vector<int> data;

public:
    MyClass(const std::string& name);
    void addData(int grade);
    int calculateAverage() const;
    std::string getName() const;
};

#endif

// ========== MyClass.cpp (source) ==========
#include "MyClass.h"
#include <numeric>
#include <stdexcept>

MyClass::MyClass(const std::string& name) : name(name) {}

void MyClass::addData(int grade) {
    data.push_back(grade);
}

int MyClass::calculateAverage() const {
    if (data.empty()) {
        throw std::runtime_error("Empty data");
    }
    int total = std::accumulate(data.begin(), data.end(), 0);
    return total / data.size();
}

std::string MyClass::getName() const {
    return name;
}

// ========== main.cpp ==========
#include "MyClass.h"
#include <iostream>

int main() {
    MyClass obj("Sensor A");
    obj.addData(85);
    obj.addData(90);
    obj.addData(78);

    std::cout << obj.getName() << ": " << obj.calculateAverage() << std::endl;
    return 0;
}

Include Guards vs #pragma once

// ===== Include Guards (portable, all compilers) =====
#ifndef UTILS_H
#define UTILS_H

// declarations...
#endif

// ===== #pragma once (modern, simpler) =====
#pragma once

// declarations...

Comparison

Aspect Include Guards #pragma once
Portability All compilers Most compilers
Simplicity Requires manual defines One line
Name collisions Can happen Cannot happen
Recommendation Legacy projects New projects

Naming Conventions

// Standard C++ naming conventions

// Classes/Structs: PascalCase
class UserService {};
struct UserData {};

// Functions: camelCase or PascalCase
void calculateTotal();
int GetUserAge();

// Variables: camelCase
int studentCount;
string firstName;

// Constants: kCamelCase or UPPER_CASE
constexpr int kMaxRetries = 3;
const double PI = 3.14159;

// Member variables: trailing underscore or m_ prefix
class Account {
    double balance_;    // trailing underscore
    double m_balance;   // m_ prefix
};

// Enums: PascalCase enum, camelCase or UPPER_CASE values
enum class Color {
    Red,
    Green,
    Blue
};

enum class ErrorCode {
    NONE = 0,
    FILE_NOT_FOUND = 1,
    INVALID_INPUT = 2
};

// Namespaces: snake_case
namespace my_library {
    class Widget {};
}

// Template parameters: PascalCase
template<typename T, typename Allocator>
class Container {};

Modern C++ Best Practices

#include <iostream>
#include <vector>
#include <string>
#include <memory>
#include <algorithm>
using namespace std;

// 1. Use auto for complex types
void exampleAuto() {
    auto data = vector<int>{1, 2, 3, 4, 5};
    auto it = data.begin();

    // 2. Use range-based for
    for (const auto& val : data) {
        cout << val << " ";
    }
    cout << endl;
}

// 3. Use smart pointers, not raw pointers
class Resource {
public:
    void use() { cout << "Resource used" << endl; }
};

void exampleSmartPointer() {
    // Owned — use unique_ptr
    auto owned = make_unique<Resource>();
    owned->use();

    // Shared — use shared_ptr
    auto shared1 = make_shared<Resource>();
    auto shared2 = shared1; // reference count = 2

    // DON'T: Resource* r = new Resource();
}

// 4. Use enum class, not plain enum
enum class Direction { Up, Down, Left, Right };

void exampleEnum() {
    Direction dir = Direction::Up;
    if (dir == Direction::Up) {
        cout << "Naik" << endl;
    }
}

// 5. Use nullptr, not NULL or 0
void exampleNullptr() {
    int* ptr = nullptr;
    // int* ptr = NULL; // DON'T
}

// 6. Use const & for large parameters
void process(const string& data) {
    cout << data.length() << endl;
}

// 7. Use initializer lists
vector<int> createVector() {
    return {1, 2, 3, 4, 5};
}

int main() {
    exampleAuto();
    exampleSmartPointer();
    exampleEnum();
    exampleNullptr();
    process("Hello World");
    auto v = createVector();
    cout << "Size: " << v.size() << endl;

    return 0;
}

Performance Tips

Move Semantics

#include <iostream>
#include <string>
#include <vector>
using namespace std;

// BAD — a copy happens
vector<int> createData() {
    vector<int> data = {1, 2, 3, 4, 5};
    return data; // NRVO or a copy
}

// BETTER — move semantics
void processData(vector<int>&& data) {
    // data can be "stolen"
    cout << "Size: " << data.size() << endl;
}

int main() {
    // Move constructor
    string a = "Hello World";
    string b = move(a); // a is now valid but unspecified
    cout << "b: " << b << endl;
    // cout << "a: " << a << endl; // DON'T — a has been moved

    // Perfect forwarding
    vector<int> data = createData();
    processData(move(data)); // data is moved, not copied
    cout << "After move, size: " << data.size() << endl; // 0

    return 0;
}

Reserve for Vector

#include <iostream>
#include <vector>
using namespace std;

int main() {
    // BAD — multiple reallocations
    vector<int> bad;
    for (int i = 0; i < 100000; i++) {
        bad.push_back(i); // may reallocate many times
    }

    // GOOD — a single allocation
    vector<int> good;
    good.reserve(100000); // allocate once
    for (int i = 0; i < 100000; i++) {
        good.push_back(i); // no reallocation needed
    }

    // BAD — inserting in the middle
    vector<int> data;
    data.reserve(100000);
    for (int i = 0; i < 100000; i++) {
        data.insert(data.begin(), i); // O(n) per insert!
    }

    // GOOD — push_back then reverse
    vector<int> data2;
    data2.reserve(100000);
    for (int i = 0; i < 100000; i++) {
        data2.push_back(i); // O(1) amortized
    }
    reverse(data2.begin(), data2.end());

    return 0;
}

Emplace vs Push

#include <iostream>
#include <vector>
#include <string>
using namespace std;

int main() {
    vector<pair<string, int>> data;

    // BAD — a temporary pair is created then copied
    data.push_back(make_pair("Mike", 90));

    // GOOD — constructed in-place
    data.emplace_back("John", 85);

    // For strings
    vector<string> names;

    // BAD — a temporary string is created
    names.push_back(string("Hello"));

    // GOOD — constructed in-place
    names.emplace_back("Hello");

    cout << "Data: ";
    for (const auto& [name, score] : data) {
        cout << name << "=" << score << " ";
    }
    cout << endl;

    return 0;
}

Constness and Efficiency

#include <iostream>
#include <vector>
using namespace std;

// BAD — return by value (potential copy)
vector<int> getNumbersBad() {
    vector<int> result = {1, 2, 3, 4, 5};
    return result;
}

// BETTER — const reference (no copy)
void printVector(const vector<int>& data) {
    for (const auto& val : data) {
        cout << val << " ";
    }
    cout << endl;
}

// GOOD — move semantics when transferring ownership
vector<int> createLargeVector() {
    vector<int> data(10000);
    // fill data...
    return data; // move or NRVO
}

int main() {
    vector<int> nums = createLargeVector();
    printVector(nums);

    // Iterators
    // BAD — const_iterator when you don't need modification
    // GOOD — auto with const
    for (const auto& val : nums) {
        // read-only
    }

    return 0;
}

Useful Libraries and Tools

Libraries to Know

Library Function Website
fmt String formatting github.com/fmtlib/fmt
spdlog Logging github.com/gabime/spdlog
nlohmann/json JSON parsing github.com/nlohmann/json
Catch2 Unit testing github.com/catchorg/Catch2
Google Test Unit testing github.com/google/googletest
CMake Build system cmake.org

Build System: CMake Basics

# CMakeLists.txt
cmake_minimum_required(VERSION 3.14)
project(MyProject VERSION 1.0 LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

add_executable(myapp
    src/main.cpp
    src/MyClass.cpp
)

target_include_directories(myapp PRIVATE include)
target_compile_options(myapp PRIVATE -Wall -Wextra -Werror)

Common Mistakes and Solutions

1. Integer Overflow

#include <iostream>
using namespace std;

int main() {
    int a = 2147483647; // INT_MAX
    // int b = a + 1;   // OVERFLOW — undefined behavior!

    // SOLUTION: use a type large enough
    long long b = static_cast<long long>(a) + 1;
    cout << "a + 1 = " << b << endl;

    // Or use checked arithmetic
    if (__builtin_add_overflow(a, 1, &a)) {
        cout << "Overflow detected!" << endl;
    }

    return 0;
}

2. Dangling Reference

#include <iostream>
#include <string>
using namespace std;

string& getRef() {
    string local = "Hello";
    return local; // DANGLING REFERENCE!
}

int main() {
    // string& ref = getRef(); // UNDEFINED BEHAVIOR!

    // SOLUTION: return by value
    string getValue() {
        string local = "Hello";
        return local; // copy or move
    }

    cout << getValue() << endl;

    return 0;
}

3. Slicing

#include <iostream>
using namespace std;

class Base {
public:
    virtual void whoami() { cout << "Base" << endl; }
};

class Derived : public Base {
public:
    void whoami() override { cout << "Derived" << endl; }
};

int main() {
    // BAD — slicing
    Base obj = Derived(); // Derived is sliced into a Base
    obj.whoami(); // "Base"

    // GOOD — pointer/reference
    Derived d;
    Base& ref = d;
    ref.whoami(); // "Derived"

    Base* ptr = new Derived();
    ptr->whoami(); // "Derived"
    delete ptr;

    return 0;
}

Pre-Deployment Checklist

  • [ ] -Wall -Wextra -Werror enabled
  • [ ] No warnings
  • [ ] Valgrind: no memory leaks
  • [ ] All tests passing
  • [ ] -O2 or -O3 for release
  • [ ] Smart pointers used, not raw pointers
  • [ ] const used where appropriate
  • [ ] Move semantics utilized
  • [ ] Complete error handling
  • [ ] Include guards or #pragma once present

Summary

C++ tips and tricks cover the entire development cycle: compiler flags for clean code, debugging tools for finding bugs, performance techniques for optimization, code organization for maintainability, and best practices for robust code. Combining mastery of these aspects makes for an effective and professional C++ developer.