Open Source in C++: From Reading Code to Your First Pull
이 글의 핵심
Contribute to C++ open source: choose the right project, fork and build locally, write your fix with tests, submit a clean PR, and survive code review. Practical examples using spdlog and fmt.
Why Contribute to Open Source?
Working on open source C++ libraries accelerates your skills in ways that proprietary codebases rarely do. You read code written by experienced engineers with different styles, submit to review by people with no obligation to be gentle, and learn how professional C++ projects handle CI, testing, and compatibility. A public PR record also matters when job-hunting — hiring managers can read your actual code.
The barrier is psychological, not technical. Most “good first issue” fixes take a few hours and require understanding only a small slice of the codebase.
Choosing the Right Project
Not every project is beginner-friendly. Look for:
| Signal | What to check |
|---|---|
| Active maintenance | Recent commits, issues responded to within days |
| CONTRIBUTING.md | Detailed build instructions, PR checklist, style guide |
| Good first issue labels | Curated entry points with clear scope |
| CI green on main | If CI is already broken, your PR will fight noise |
| Reasonable review culture | Skim recent PR discussions for tone |
Good C++ starting points:
| Project | Why it’s beginner-friendly |
|---|---|
| spdlog | Small, well-tested logging library; clear code; welcoming maintainer |
| fmt | Header-heavy but modular; detailed issue descriptions |
| Catch2 | Test framework; contributors often start with test-writing PRs |
| nlohmann/json | Enormous user base; many small, well-described issues |
| vcpkg ports | Adding or updating a package port is self-contained and reviewed quickly |
Avoid starting with: LLVM, GCC, Boost (large bureaucracy), or security-critical crypto libraries.
The Fork and Clone Workflow
Every open source contribution follows the same Git workflow:
# 1. Fork on GitHub (click Fork button on the project page)
# This creates YOUR copy at github.com/YOUR_USERNAME/spdlog
# 2. Clone YOUR fork (not the upstream)
git clone https://github.com/YOUR_USERNAME/spdlog.git
cd spdlog
# 3. Add upstream remote so you can pull future changes
git remote add upstream https://github.com/gabime/spdlog.git
# Verify remotes
git remote -v
# origin https://github.com/YOUR_USERNAME/spdlog.git (fetch)
# origin https://github.com/YOUR_USERNAME/spdlog.git (push)
# upstream https://github.com/gabime/spdlog.git (fetch)
# upstream https://github.com/gabime/spdlog.git (push)
# 4. Fetch upstream to see what's on their main
git fetch upstream
Building and Running Tests
Before touching any code, get the project building and tests passing locally:
# spdlog CMake build
cmake -B build -DSPDLOG_BUILD_TESTS=ON -DCMAKE_BUILD_TYPE=Debug
cmake --build build -j$(nproc)
cd build && ctest --output-on-failure
# fmt CMake build
cmake -B build -DFMT_TEST=ON
cmake --build build -j$(nproc)
cd build && ctest --output-on-failure
If the build fails, check CONTRIBUTING.md for required dependencies. Common issues:
# Missing fmt dependency (spdlog often bundles it, but confirm)
git submodule update --init --recursive
# Compiler version mismatch
cmake -B build -DCMAKE_CXX_STANDARD=17 -DCMAKE_CXX_COMPILER=clang++
Run the full test suite before making any changes. You need a green baseline — if a test fails before your change, that is not your bug to fix (but you can note it).
Finding and Understanding the Issue
Once local builds are green, find an issue to work on:
# GitHub search for C++ good first issues
https://github.com/search?q=is:issue+is:open+label:"good+first+issue"+language:C%2B%2B
When you find a candidate:
- Read the issue fully — understand the expected vs. actual behavior
- Find the relevant code:
git grep,find . -name "*.h", or use your IDE’s symbol search - Reproduce the problem: write a tiny
test.cppthat demonstrates the bug, or run the failing test - Read the test file for the affected module — you will add a test alongside your fix
# In spdlog: find files related to "async" logging
grep -r "async_logger" include/ --include="*.h" -l
grep -r "async_logger" tests/ --include="*.cpp" -l
Creating Your Branch
Never work on main. Create a descriptive branch name:
# Sync your fork's main with upstream first
git fetch upstream
git checkout main
git merge upstream/main # fast-forward if no local commits
git push origin main # keep your fork's main in sync
# Create a topic branch
git checkout -b fix/async-logger-flush-on-destroy
# or
git checkout -b docs/add-sinks-example
# or
git checkout -b feat/add-daily-file-sink-rotation
Writing the Fix
A concrete example: suppose spdlog has a bug where the async logger does not flush on destruction. You find the destructor in include/spdlog/async_logger-inl.h:
// Before (broken)
SPDLOG_INLINE spdlog::async_logger::~async_logger() {
// does nothing — messages in the queue may be lost
}
// After (fixed)
SPDLOG_INLINE spdlog::async_logger::~async_logger() {
SPDLOG_TRY {
flush(); // drain the queue before the thread pool shuts down
}
SPDLOG_LOGGER_CATCH(source_loc{})
}
Write a test that covers your fix:
// tests/test_async.cpp — add a new test case
TEST_CASE("async logger flushes on destruction", "[async]") {
auto tp = std::make_shared<spdlog::details::thread_pool>(8, 1);
auto sink = std::make_shared<spdlog::sinks::test_sink_mt>();
{
auto logger = std::make_shared<spdlog::async_logger>(
"test", sink, tp, spdlog::async_overflow_policy::block);
logger->info("message 1");
logger->info("message 2");
// destructor called here — should flush
}
// Messages must have reached the sink
REQUIRE(sink->msg_counter() == 2);
}
Rebuild and run:
cmake --build build -j$(nproc)
cd build && ctest -R "async" --output-on-failure
Style and Formatting
Before committing, apply the project’s formatter:
# clang-format — most C++ projects use it
clang-format -i include/spdlog/async_logger-inl.h tests/test_async.cpp
# Check for issues without modifying
clang-format --dry-run --Werror file.cpp
# If the project has a script
./scripts/run-clang-format.sh
Check the .clang-format file in the root. Never override formatting choices — even if you disagree, a consistent style across the codebase is more important than your preference.
Committing with Conventional Commits and DCO
Most C++ projects use Conventional Commits for structured history:
type(scope): short imperative description
Longer explanation if needed.
Fixes #1234
Types: fix, feat, docs, refactor, test, perf, ci, chore
With DCO sign-off (-s):
git add include/spdlog/async_logger-inl.h tests/test_async.cpp
git commit -s -m "fix(async_logger): flush queue on destruction
Without an explicit flush() call in the destructor, messages buffered
in the async queue could be dropped when the logger went out of scope
before the thread pool finished processing them.
Fixes #789"
The -s flag appends a Signed-off-by: Your Name <[email protected]> line. Many projects check for this in CI and will reject PRs without it.
Opening the Pull Request
Push your branch:
git push -u origin fix/async-logger-flush-on-destroy
Then open a PR on GitHub. A good PR description:
## Summary
The async logger destructor did not call `flush()`, causing messages in
the async queue to be dropped when the logger went out of scope before
the thread pool finished processing them.
## Changes
- `async_logger::~async_logger()`: added `flush()` inside SPDLOG_TRY block
- `tests/test_async.cpp`: added test case `async logger flushes on destruction`
## How to test
```bash
cmake --build build && cd build && ctest -R "async" --output-on-failure
Fixes #789
Keep PRs **single-purpose**: one bug = one PR. Reviewers won't merge a PR that fixes a bug AND reorganizes unrelated code — it makes review harder and bisect messier.
---
## Surviving Code Review
Reviewers will ask for changes. Common feedback types:
**Style changes**: easy — just apply the requested format.
**Test coverage**: add more test cases to exercise edge cases.
**API questions**: the reviewer thinks your fix changes a public interface in a breaking way — discuss in the PR thread.
**Alternative approach**: the reviewer suggests a fundamentally different implementation. Ask why before rewriting — sometimes it is preference, sometimes it is a real constraint.
Responding:
```bash
# After making requested changes
git add -u
git commit -s -m "review: address feedback from @maintainer
- move flush() call inside SPDLOG_TRY block
- add test for overflow_policy::overrun_oldest behavior"
git push origin fix/async-logger-flush-on-destroy
# The PR updates automatically
Never force-push an open PR unless explicitly asked. It breaks review context.
Common Errors and Fixes
| Error | Cause | Fix |
|---|---|---|
| CMake can’t find dependency | Missing system package | Check README; install with apt/brew/vcpkg |
| CI fails clang-format | Formatting difference | clang-format -i your changed files |
| CI fails on Windows but passes locally | Path separator, MSVC extension | Test on Windows or use GitHub Actions matrix |
| DCO check fails | Missing Signed-off-by line | git commit --amend -s or git rebase --signoff |
| Merge conflict after rebase | Upstream moved while you worked | git fetch upstream && git rebase upstream/main |
| Test fails on CI but passes locally | Environment difference | Check if test uses hardcoded paths, clocks, or OS APIs |
What Happens After Your PR is Merged
- Your commit SHA is now in the official history of a public library
- You are listed in the contributors graph
- You understand that slice of the codebase deeply — good for mentioning in interviews
- You have a template for every future contribution to any project
The first merge is the hardest. After that, the workflow is muscle memory and the review process stops feeling intimidating.
Key Takeaways
- Start small: documentation, test additions, and well-scoped bugfixes are ideal first PRs
- Build before you touch anything — you need a green baseline to prove your change didn’t break existing tests
- One issue per PR: maintainers won’t review a PR that does multiple things
- Conventional Commits + DCO:
git commit -s -m "fix(scope): ..."is the standard pattern in most C++ projects - clang-format before push: CI will catch it anyway — fix it locally first
- Never force-push an open PR — it breaks review context
- Rejections are normal: fold the feedback, pick another issue, keep going
자주 묻는 질문 (FAQ)
Q. 이 내용을 실무에서 언제 쓰나요?
A. Contribute to famous C++ libraries: pick issues, fork workflow, Conventional Commits, CI, DCO, and review culture. Pract… 실무에서는 위 본문의 예제와 선택 가이드를 참고해 적용하면 됩니다.
Q. 선행으로 읽으면 좋은 글은?
A. 각 글 하단의 이전 글 또는 관련 글 링크를 따라가면 순서대로 배울 수 있습니다. C++ 시리즈 목차에서 전체 흐름을 확인할 수 있습니다.
Q. 더 깊이 공부하려면?
A. cppreference와 해당 라이브러리 공식 문서를 참고하세요. 글 말미의 참고 자료 링크도 활용하면 좋습니다.
같이 보면 좋은 글 (내부 링크)
이 주제와 연결되는 다른 글입니다.
- C++ 커스텀 컴파일러 패스 | Clang 플러그인·AST 변환·커스텀 진단 [#55-6]
- C++ CI/CD 파이프라인: GitHub Actions를 이용한 멀티 OS 자동 빌드·테스트 가이드
- C++ 정적 분석 도구 통합: Clang-Tidy와 Cppcheck로 코드 퀄리티 강제하기 [#41-1]
이 글에서 다루는 키워드 (관련 검색어)
C++, Open Source, Contribution, Pull Request, GitHub, spdlog, fmt 등으로 검색하시면 이 글이 도움이 됩니다.