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
// ========== 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;
}
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;
}
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.