C++中如何为变量读写添加前置检查?调试模式下线程安全访问控制实现方案
Great question! Let's break down how to build this system step by step, focusing on zero release overhead and keeping syntax as natural as possible.
Core Concepts
We need two key components to make this work:
- A
Resource<T>wrapper that intercepts read/write operations only in debug mode. - An
AccessPolicysystem that tracks allowed resource access for each task, paired with thread-local storage to enforce checks during task execution.
Step 1: Define Access Policy and Thread-Local Tracking
First, we'll represent access permissions and track which policy is active for the current task:
#include <unordered_map> #include <functional> #include <cassert> enum class EAccessPolicy { Read, Write, ReadWrite }; // Tracks which resources a task is allowed to access and how class AccessPolicy { private: std::unordered_map<const void*, EAccessPolicy> m_permissions; public: template<typename T> void AddResource(const Resource<T>& resource, EAccessPolicy policy) { // Use the resource's address as a unique identifier m_permissions[&resource] = policy; } bool allows_read(const void* resource) const { auto it = m_permissions.find(resource); return it != m_permissions.end() && (it->second == EAccessPolicy::Read || it->second == EAccessPolicy::ReadWrite); } bool allows_write(const void* resource) const { auto it = m_permissions.find(resource); return it != m_permissions.end() && (it->second == EAccessPolicy::Write || it->second == EAccessPolicy::ReadWrite); } }; // Thread-local storage to track the active task's access policy inline thread_local const AccessPolicy* g_current_policy = nullptr;
Step 2: Implement the Task Class
The Task class manages running functions with their associated access policy, ensuring the policy is active only during task execution:
class Task { private: std::function<void()> m_task_func; AccessPolicy m_policy; public: Task(std::function<void()> func, AccessPolicy policy) : m_task_func(std::move(func)), m_policy(std::move(policy)) {} void run() { // Save the old policy to restore later const AccessPolicy* old_policy = g_current_policy; g_current_policy = &m_policy; try { m_task_func(); } catch (...) { // Restore policy even if the task throws g_current_policy = old_policy; throw; } g_current_policy = old_policy; } };
Step 3: Implement the Resource Wrapper
The Resource<T> class behaves differently in debug vs release mode:
- Release Mode: Thin wrapper with direct access to
T(zero overhead). - Debug Mode: Restricts direct access, using proxies to intercept reads/writes and enforce policy checks.
template<typename T> struct Resource { private: T m_data; #ifdef NDEBUG // Release mode: full, direct access with no overhead public: T& operator*() { return m_data; } const T& operator*() const { return m_data; } T* operator->() { return &m_data; } const T* operator->() const { return &m_data; } operator T&() { return m_data; } operator const T&() const { return m_data; } T* operator&() { return &m_data; } const T* operator&() const { return &m_data; } Resource& operator=(const T& other) { m_data = other; return *this; } Resource& operator=(T&& other) { m_data = std::move(other); return *this; } #else // Debug mode: proxy-based access with policy checks private: void check_read() const { if (!g_current_policy || !g_current_policy->allows_read(this)) { assert(false && "Unauthorized read access to resource"); // Optional: Log "read" event here } } void check_write() { if (!g_current_policy || !g_current_policy->allows_write(this)) { assert(false && "Unauthorized write access to resource"); // Optional: Log "write" event here } } public: // Proxy for const (read-only) access struct ConstProxy { const Resource<T>* m_resource; const T& m_data; ConstProxy(const Resource<T>* res, const T& data) : m_resource(res), m_data(data) {} // Call const member functions template<typename Ret, typename... Args> Ret call(Ret (T::*func)(Args...) const, Args&&... args) const { m_resource->check_read(); return (m_data.*func)(std::forward<Args>(args)...); } // Access const member variables template<typename Member> const Member& get(const Member T::*member) const { m_resource->check_read(); return m_data.*member; } }; // Proxy for mutable (write) access struct MutableProxy { Resource<T>* m_resource; T& m_data; MutableProxy(Resource<T>* res, T& data) : m_resource(res), m_data(data) {} // Call non-const member functions template<typename Ret, typename... Args> Ret call(Ret (T::*func)(Args...), Args&&... args) { m_resource->check_write(); return (m_data.*func)(std::forward<Args>(args)...); } // Access non-const member variables template<typename Member> Member& get(Member T::*member) { m_resource->check_write(); return m_data.*member; } }; // Get proxies for read/write access ConstProxy read() const { return ConstProxy(this, m_data); } MutableProxy write() { return MutableProxy(this, m_data); } // Intercept assignment to the entire resource Resource& operator=(const T& other) { check_write(); m_data = other; return *this; } Resource& operator=(T&& other) { check_write(); m_data = std::move(other); return *this; } #endif };
Step 4: Macros for Natural Syntax (Debug Mode)
To keep debug-mode syntax close to your original example, add these helper macros:
#ifdef NDEBUG #define READ_CALL(res, func) (res->func) #define READ_MEMBER(res, member) (res->member) #define WRITE_CALL(res, func) (res->func) #define WRITE_MEMBER(res, member) (res->member) #else #define READ_CALL(res, func) res.read().call(&func) #define READ_MEMBER(res, member) res.read().get(&member) #define WRITE_CALL(res, func) res.write().call(&func) #define WRITE_MEMBER(res, member) res.write().get(&member) #endif
Step 5: Example Usage
Now you can use the system as intended (with natural syntax in both modes):
#include <cstdint> class SomeType { public: uint32_t GetSomething() const { return m_something; } uint32_t somethingElse; private: uint32_t m_something = 0; }; Resource<SomeType> resource1; Resource<SomeType> resource2; Resource<SomeType> resource3; static void ExampleTask() { // Allowed read access auto something = READ_CALL(resource1, SomeType::GetSomething)(); // Allowed write access (assign entire object) resource2 = SomeType(); // Allowed write access to member WRITE_MEMBER(resource2, SomeType::somethingElse) = 4; // Unauthorized read access (triggers assert in debug mode) READ_CALL(resource3, SomeType::GetSomething)(); } int main() { AccessPolicy accessPolicy; accessPolicy.AddResource(resource1, EAccessPolicy::Read); accessPolicy.AddResource(resource2, EAccessPolicy::Write); Task task(ExampleTask, accessPolicy); task.run(); return 0; }
Key Notes
- Release Overhead: In release mode (
NDEBUGdefined),Resource<T>is a thin wrapper with no extra checks or overhead—just direct access toT. - Debug Checks: All read/write operations are intercepted in debug mode, with assertions triggering on unauthorized access.
- Limitations: C++ doesn't allow overloading the
.operator, so we use proxies/macros to intercept member access. This is the most robust way to enforce checks without changing core syntax.
Content of the question comes from Stack Exchange, asked by Storm Søndergaard

