LaunchDarkly C++ SDKs monorepo: client SDK, server SDK, server SDK integrations (Redis, OTel), plus shared libraries. See README.md for the build/install reference and CONTRIBUTING.md for the style basics (Google C++ style, C++17, C99).
Full reference: README.md. A few things that aren't obvious from it:
cmake -B build -S .— always pass an out-of-source build directory with-B. Don't run cmake with.as the build directory; it doesn't work cleanly here.- Multiple build configurations can coexist as sibling build directories, e.g. one with sanitizers on, one with
LD_CURL_NETWORKING=ON. A common local convention isbuild-nosan/andbuild-curl/, but don't assume those directories exist — list the directory first before using one. - If test binaries hang at startup with no output, that's an intermittent ASAN issue seen on some platforms. Try a no-sanitizer build (
-DLD_TESTING_SANITIZERS=OFF) to confirm. Not universal — don't assume it's always the cause. - When SSE or HTTP networking code changes, validate against both backends: Boost.Beast/Foxy (default) and CURL (
-DLD_CURL_NETWORKING=ON). - Always wrap test runs with
timeout: e.g.timeout 15 <build>/gtest_launchdarkly-cpp-internal. Deadlocks don't fail GTest — they hang forever. - After adding a brand-new test file, re-run
cmake -S . -B <build-dir>beforecmake --build. The test CMakeLists usefile(GLOB), which only re-globs at configure time; without a re-configure, the new file is silently skipped. - Test target names:
gtest_launchdarkly-cpp-{common,internal,client,server,sse-client}. Note the-cpp-serverform — NOTgtest_launchdarkly-server-sdk. - Test binaries land at the build root (
<build>/gtest_…), not nested under the source tree'stests/directory.
libs/client-sdk and libs/server-sdk are distinct SDKs sharing libraries (libs/common, libs/internal, libs/server-sent-events). Public APIs and data models differ. Always state which SDK(s) a change applies to — the answer is rarely "both," even when the feature name is shared.
libs/— SDK and shared libraries.architecture/— design docs for major subsystems; read before non-trivial changes in those areas.vendor/— third-party source with project-specific patches; don't edit casually.contract-tests/,examples/,scripts/,cmake/,cmake-tests/— as named.
Basics (Google C++ style, naming, #pragma once, .cpp/.hpp for C++, .c/.h for C) are in CONTRIBUTING.md. Repo-specific rules below.
""for headers in this library's own source tree (sibling headers, private internal headers).<>for the library's installed public API (<launchdarkly/...>), third-party (Boost, etc.), and standard library.- Group order: std/system, then third-party, then
<launchdarkly/...>, then"local". Blank line between groups. (Matches existing code.)
- Anything under
libs/*/include/launchdarkly/...is public. Avoid breaking changes. If a public signature must change, call it out explicitly in the PR description. - Shared-library builds export only the C API. Don't change symbol visibility without understanding why.
- Public methods returning
boolare namedIsX()/HasX(), notX().
- For mutable (out / in-out) parameters, use
T*, notT&. The pointer makes mutation visible at the call site (&foovsfoo). Const references (const T&) for read-only are fine. - For literal arguments (
nullptr,true,false, integer/duration literals), prefix with/* name= */when the parameter's role isn't obvious. Skip when the function name + literal type already convey it (WaitForResult(1s),unique_ptr(nullptr),vec.reserve(100)).
- Default to one-line comments. If you're writing a paragraph, either the name needs to do more work or no comment is needed.
- Non-trivial classes need a doc comment that explicitly states the thread-safety contract (thread-safe / not thread-safe / partial, with specifics on which methods are safe from which contexts).
- Public methods on non-trivial classes need a doc comment explaining how callers should use them: preconditions, side effects, threading constraints, return semantics. Document what callers need, not implementation details (don't expose
shared_ptr, backends, internal resource management). - No Java references. American spellings.
- No session/roadmap labels in comments ("Step 7", "phase 2"). Use
TODO:plus a description.
shared_ptris not the default. Each use needs a concrete justification (genuine shared ownership, async callbacks outliving the owner, type-erased callable storage). Default to value,unique_ptr, or non-owningT*with a documented lifetime contract.- Don't take a constructor parameter as
T&and store it as a member reference. Useshared_ptr(with justification),T*plus a constructor doc comment on lifetime, or take ownership.
boost::asio::io_contextdoes not serialize callbacks by itself — it can be run from multiple threads. When a thread-safety comment relies on serialization, name the actual mechanism: strand, mutex, single-thread invariant, or sequential.Then()chaining.- Inside thread-safe classes, group member variables by what protects them (
const,protected by mutex_, etc.) and annotate each group. - Default lock scope is the whole function with one
std::lock_guardat the top. Only narrow with a stated reason (re-entrant callback, blocking I/O, lock ordering) — write the reason in a comment. - A pointer/reference derived from mutex-protected state inherits the protection. Don't copy a raw pointer out under the lock and use it after releasing.
- When chaining with
Future::Then, use the flattening overload. Don't capture aPromisein the continuation to resolve manually.
- Method definitions in
.cppfollow the order of declarations in the.hpp.
- GoogleTest. Match the style of the adjacent test file you're modifying.
- Default to inline construction of test data. Helpers are acceptable when they substantially reduce boilerplate repeated across many tests — not just for "I might call this twice."
- Don't sleep then assert a negative ("future didn't fire", "callback not called"). If nothing else can resolve the future, check state synchronously.
- Skip
// Act/// Assertlabels — write a short descriptive comment of what the block does or expects. - Don't pad timer durations "for CI safety" in drain-then-assert tests; 5ms is fine when there's no race to lose.
Doxygen, configured per-library via a Doxyfile. Build with scripts/build-docs.sh <library-dir>. Public APIs get Doxygen comments describing behavior callers depend on — not implementation details.
clang-format -i <file> (config is in the repo).
- Commit messages: single-line conventional commits (
feat:,fix:,refactor:,docs:,chore:,test:). No body unless asked. - PR descriptions: terse. One-sentence summary, a few bullets for what's in, design decisions only for non-obvious choices. Add a "not in scope" line only when a reviewer would reasonably expect that thing to be in this PR — don't manufacture exclusions.