C++ Developer
Write new C++ code that follows CorsixModStudio codebase conventions across all layers, preferring modern C++20 practices wherever it is safe to do so.
Boy-scout rule: When touching existing code, improve it toward modern C++ if the change
is safe and localized (e.g., NULL → nullptr, add override, use range-for, add const,
replace raw arrays with std::vector). Keep modernization changes within the scope of the
files you're already modifying — don't refactor unrelated code.
Workflow
- •
Determine which layer the new code belongs to:
Rainman (
src/rainman/) → Consult references/rainman-patterns.md CDMS (src/cdms/) → Consult references/cdms-patterns.md Vendored Lua (src/rainman/lua502/) → Consult references/lua-rules.md - •
Apply the common conventions below to all new code.
- •
Default to modern C++20 — see the Modern C++ Practices section. Fall back to legacy style only at existing API boundaries. When modifying existing code, apply boy-scout improvements (add
override,nullptr,const, range-for, etc.) within the touched scope. - •
After writing code, add new files to the appropriate
CMakeLists.txt. - •
Build and verify:
cmake --build --preset debug
File Header
Every new .h and .cpp file MUST begin with the LGPL v2.1 copyright block:
/* Rainman Library Copyright (C) 2006 Corsix <corsix@gmail.com> This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */
Naming Conventions
| Element | Convention | Example |
|---|---|---|
| Concrete class | C prefix | CSgaFile, CMemoryStore |
| Interface/abstract class | I prefix | IFileStore, IMetaTable |
| Member variable | m_ + Hungarian prefix | m_sMessage (string), m_iLine (integer), m_pPrecursor (pointer), m_bInited (bool) |
| Virtual method | V prefix | VInit, VRead, VSeek, VOpenStream |
| Include guard | _UPPER_SNAKE_H_ | _C_SGA_FILE_H_ |
Hungarian prefixes: s = string/char*, i = integer/unsigned long, p = pointer, b = bool, f = float, ws = wchar_t*, t = table/vector, o = object.
Error Handling
This codebase uses heap-allocated exception objects, not stack-based C++ exceptions. Exceptions are created with new and cleaned up with destroy(), never delete.
Throwing
// Simple throw
throw new CRainmanException(__FILE__, __LINE__, "message");
// Or use macros:
QUICK_THROW("message"); // throw new CRainmanException(...)
CATCH_THROW("context message"); // catch + re-throw with precursor
Catching and Cleanup
try {
SomeOperation();
} catch (CRainmanException* pE) {
pE->destroy(); // CRITICAL: always destroy(), never delete
}
// Or use macros:
try { SomeOperation(); } IGNORE_EXCEPTIONS // catch + destroy
try { SomeOperation(); } CATCH_THROW("Failed during X") // catch + re-throw
Memory Allocation Checks
char* pBuffer = CHECK_MEM(new char[1024]); // throws if allocation fails
Memory Management
New code (preferred)
Use RAII and smart pointers for all new internal code:
// Prefer std::unique_ptr for owned heap objects
auto pBuffer = std::make_unique<char[]>(1024);
// Use a custom deleter for IStream* returned by VOpenStream()
auto pStream = std::unique_ptr<IFileStore::IStream>(store.VOpenStream("path"));
pStream->VRead(1, sizeof(unsigned long), &iValue);
// stream is automatically deleted when pStream goes out of scope
// Use a custom deleter for CRainmanException* if you need to hold one
auto pEx = std::unique_ptr<CRainmanException, decltype(&CRainmanException::destroy)>(
pCaught, &CRainmanException::destroy);
Legacy API boundaries
When implementing or overriding existing virtual interfaces that return raw pointers
(e.g., VOpenStream(), VOpenOutputStream()), continue to return raw new-allocated
objects — callers of those interfaces are responsible for deletion.
Within the implementation body, still prefer smart pointers for intermediate allocations.
- •Use
CHECK_MEM(new ...)for allocations that must not fail at API boundaries. - •Use
AutoDelete<T>(fromInternal_Util.h) when interfacing with legacy cleanup patterns.
RAINMAN_API Macro
All public Rainman classes MUST use the RAINMAN_API macro on their class declaration. It currently expands to nothing (static lib build) but must remain for future DLL compatibility:
#include "Api.h"
class RAINMAN_API CMyNewClass
{
// ...
};
Nested public classes also need RAINMAN_API:
class RAINMAN_API CMyClass : public IFileStore
{
public:
class RAINMAN_API CStream : public IFileStore::IStream { ... };
};
Modern C++ Practices
Default to modern C++20 in all new code. When editing existing code, apply boy-scout improvements within the scope of your change. Only preserve legacy style at existing API boundaries where changing would cascade.
Smart pointers & RAII
- •Prefer
std::unique_ptr/std::make_uniquefor owned heap objects. - •Wrap raw-pointer returns from legacy APIs (e.g.,
VOpenStream()) instd::unique_ptrat the call site for automatic cleanup. - •For
CRainmanException*, use aunique_ptrwith a custom deleter that callsdestroy().
Modern types (internal / new code)
| Use | Instead of | When |
|---|---|---|
std::string / std::string_view | char* / const char* | Internal storage & parameters |
std::size_t | unsigned long | Internal sizes & loop counters |
std::uint32_t, std::int64_t | unsigned long, long | Internal fixed-width data |
std::vector<T> | T* + manual new[]/delete[] | Owning dynamic arrays |
std::array<T,N> | T[N] raw array | Fixed-size arrays |
std::optional<T> | sentinel value / out-parameter | Nullable return values |
std::span<T> (C++20) or pointer+size | T* + separate length | Non-owning views (when C++20 is available) |
API boundaries: Public virtual interfaces and methods that override existing signatures must continue using
unsigned long,char*, etc. to match.
Keywords & syntax
- •
auto— use for iterators, factory returns, lambdas, and any type that is obvious from the right-hand side. Avoidautowhen the type is not immediately clear. - •
nullptr— never useNULLor0for null pointers. - •
override— always mark virtual overrides. Addoverrideto every reimplemented virtual method. - •
const/constexpr— mark variables, parameters, and methodsconstwherever possible. Useconstexprfor compile-time constants. - •
enum class— prefer over plainenumfor new enumerations. - •
[[nodiscard]]— add to functions whose return value must not be ignored (e.g., factory functions, error codes). - •
noexcept— mark functions that are guaranteed not to throw (note: the codebase throwsCRainmanException*, so many functions cannot be noexcept).
Loops & algorithms
- •Prefer range-based for over index-based iteration:
cpp
for (const auto& item : m_tItems) { ... } - •Prefer
<algorithm>functions (std::find_if,std::transform,std::any_of, etc.) over hand-written loops when they improve clarity. - •Use structured bindings for pairs, tuples, and map entries:
cpp
for (const auto& [key, value] : myMap) { ... }
Strings
- •Prefer
std::stringfor new string storage andstd::string_viewfor non-owning read-only access. - •Use
std::format(C++20, orfmt::formatif available) oversprintf/snprintffor new formatting code if the build supports it; otherwisestd::ostringstreamorsnprintfis acceptable.
Initialization
- •Use brace initialization for aggregates and containers:
cpp
std::vector<int> ids = {1, 2, 3}; - •Prefer in-class member initializers over constructor initializer lists for default
values:
cpp
class CMyClass { bool m_bInited = false; unsigned long m_iCount = 0; };
What NOT to modernize
These legacy patterns must be preserved for compatibility — do not change them:
- •Heap-allocated exceptions (
throw new CRainmanException) — this is a project-wide convention, not a mistake. - •
RAINMAN_APImacro on public classes. - •Hungarian-prefixed member names (
m_sName,m_pStream). - •
C/Iclass prefixes andVvirtual-method prefix. - •Include guards (
_C_CLASS_NAME_H_style) — do not replace with#pragma once. - •
unsigned longin public/virtual API signatures — changing cascades. - •Vendored Lua code — never touch
lua502/orlua512/.
Legacy Type Rules (API boundaries only)
These rules apply when implementing or overriding existing public/virtual interfaces. For purely internal new code, prefer modern types (see above).
- •Use
unsigned longfor sizes and counts in existing public APIs (changing would cascade through the codebase). - •Use
longfor signed offsets in existing seek interfaces. - •Use Windows-only APIs where the codebase already does (
_wfopen,LoadLibraryW,GetProcAddress). - •Match the existing cast style (C-style or
static_cast) of the file you're editing. In new files, preferstatic_cast/reinterpret_cast/const_cast.
Build Integration
Rainman (src/rainman/CMakeLists.txt)
Sources are collected via file(GLOB), so new .cpp/.c files are auto-discovered. Just add the file and rebuild.
CDMS (src/cdms/CMakeLists.txt)
Same file(GLOB) pattern — new source files are auto-discovered.
Tests (tests/rainman/CMakeLists.txt)
Test files must be explicitly listed in the add_executable(rainman_tests ...) call:
add_executable(rainman_tests
# ... existing files ...
newfile_test.cpp # Add new test file here
)
Build & Verify
cmake --build --preset debug ctest --preset debug