Skip to content

feat: Adopt C++20 Concepts for interface type constraints #244

Description

@kcenon

Summary

Adopt C++20 Concepts to replace abstract class-based interfaces and provide compile-time type constraints for template functions. This modernization will improve type safety, provide better error messages, and enable more expressive API design.

5W1H Specification

  • Who: common_system maintainers
  • What: Introduce C++20 concepts for interface definitions and template constraints
  • Where: include/kcenon/common/interfaces/, include/kcenon/common/patterns/
  • When: v0.4.0.0 (non-breaking enhancement)
  • Why:
    • Current interfaces use abstract classes requiring runtime dispatch
    • Template functions lack explicit type constraints
    • Error messages for template misuse are cryptic
    • C++20 concepts provide compile-time duck typing with clear constraints
  • How: Define concepts alongside existing interfaces, migrate gradually

Priority

MEDIUM - Enhancement that improves developer experience without breaking existing code.

Current State Analysis

Existing Abstract Interfaces

// Current approach: abstract class with virtual methods
class IExecutor {
public:
    virtual ~IExecutor() = default;
    virtual void execute(std::function<void()> task) = 0;
    virtual void shutdown() = 0;
    virtual bool is_running() const = 0;
};

// Consumers must inherit and implement
class thread_pool_executor : public IExecutor {
    void execute(std::function<void()> task) override { /*...*/ }
    void shutdown() override { /*...*/ }
    bool is_running() const override { /*...*/ }
};

Template Functions Without Constraints

// Current: No constraints, cryptic errors if T doesn't have expected methods
template<typename T>
Result<T> map(std::function<T(U)> func);

// Current: Macros for conditional behavior
#define COMMON_RETURN_IF_ERROR(result) \
    if (!(result).is_ok()) return (result).error()

Proposed Solution

Phase 1: Define Core Concepts

// include/kcenon/common/concepts/executor.h
namespace kcenon::common::concepts {

template<typename E>
concept Executor = requires(E executor, std::function<void()> task) {
    { executor.execute(task) } -> std::same_as<void>;
    { executor.shutdown() } -> std::same_as<void>;
    { executor.is_running() } -> std::convertible_to<bool>;
};

template<typename J>
concept Job = requires(J job) {
    { job.execute() } -> std::same_as<void>;
    { job.get_name() } -> std::convertible_to<std::string_view>;
    { job.get_priority() } -> std::convertible_to<int>;
};

template<typename L>
concept Logger = requires(L logger, std::string_view msg, log_level level) {
    { logger.log(level, msg) } -> std::same_as<void>;
    { logger.is_enabled(level) } -> std::convertible_to<bool>;
};

template<typename M>
concept MetricCollector = requires(M collector, std::string_view name, double value) {
    { collector.increment(name, value) } -> std::same_as<void>;
    { collector.gauge(name, value) } -> std::same_as<void>;
    { collector.histogram(name, value) } -> std::same_as<void>;
};

} // namespace kcenon::common::concepts

Phase 2: Constrained Template Functions

// include/kcenon/common/patterns/result.h
namespace kcenon::common {

template<typename T>
concept ResultValue = std::movable<T> || std::is_void_v<T>;

template<typename F, typename T>
concept ResultMapper = requires(F f, T value) {
    { std::invoke(f, std::move(value)) };
};

template<ResultValue T>
class Result {
public:
    template<ResultMapper<T> F>
    auto map(F&& func) -> Result<std::invoke_result_t<F, T>>;

    template<typename F>
        requires std::invocable<F, T> && 
                 std::same_as<std::invoke_result_t<F, T>, Result<typename std::invoke_result_t<F, T>::value_type>>
    auto and_then(F&& func);
};

} // namespace kcenon::common

Phase 3: Concept-Based Function Overloads

// Type-safe logging that works with any Logger implementation
template<concepts::Logger L>
void log_error(L& logger, std::string_view message) {
    logger.log(log_level::error, message);
}

// Executor-agnostic task submission
template<concepts::Executor E>
void submit_task(E& executor, std::function<void()> task) {
    executor.execute(std::move(task));
}

Proposed Concepts List

Core Concepts

Concept Purpose Replaces
Executor Task execution interface IExecutor
Job Executable job unit IJob
Logger Logging interface ILogger
MetricCollector Metrics collection IMetricCollector
Monitor Health monitoring IMonitor
HttpClient HTTP client interface IHttpClient
UdpClient UDP client interface IUdpClient

Result-Related Concepts

Concept Purpose
ResultValue Valid types for Result
ResultMapper Functions that can be passed to map()
ResultBinder Functions for and_then()
ErrorHandler Functions for or_else()

Container-Related Concepts (for container_system)

Concept Purpose
Serializable Types that can be serialized
ContainerValue Valid container value types

Migration Strategy

Phase 1: Parallel Existence (Non-breaking)

// Both approaches work simultaneously
class IExecutor { /*...*/ };                    // Keep existing

template<typename E>
concept Executor = requires(E e) { /*...*/ };   // Add concept

// IExecutor implementations automatically satisfy Executor concept
static_assert(Executor<thread_pool_executor>);

Phase 2: Prefer Concepts in New Code

// New APIs use concepts by default
template<Executor E>
class task_scheduler { /*...*/ };

// Existing APIs remain unchanged for compatibility
void legacy_function(IExecutor& executor);

Phase 3: Deprecation (v0.5.0+)

// Mark abstract classes as deprecated
class [[deprecated("Use Executor concept instead")]] IExecutor { /*...*/ };

Tasks

Phase 1: Core Concepts

  • Create include/kcenon/common/concepts/ directory
  • Implement executor.h with Executor, Job concepts
  • Implement logger.h with Logger concept
  • Implement monitoring.h with MetricCollector, Monitor concepts
  • Implement transport.h with HttpClient, UdpClient concepts
  • Add umbrella header concepts.h
  • Add unit tests validating concept satisfaction

Phase 2: Result Concepts

  • Add ResultValue concept to result.h
  • Add ResultMapper concept with requires clause
  • Add ResultBinder concept for and_then
  • Constrain Result template functions
  • Improve error messages for invalid types

Phase 3: Documentation

  • Document concepts with examples
  • Add migration guide from interfaces to concepts
  • Update API reference

Phase 4: Downstream Integration

  • Update thread_system to use Executor concept
  • Update logger_system to use Logger concept
  • Update monitoring_system to use MetricCollector concept

Acceptance Criteria

  • All concepts compile with C++20 compilers (GCC 12+, Clang 14+, MSVC 19.29+)
  • Existing interface implementations satisfy corresponding concepts
  • Improved compile-time error messages for type mismatches
  • No breaking changes to existing APIs
  • Performance equivalent to abstract class approach (zero overhead)
  • Full test coverage for concept definitions

Benefits

Aspect Before (Abstract Classes) After (Concepts)
Error Messages Deep template instantiation stack Clear constraint violation
Runtime Overhead Virtual dispatch (~5-10ns) Zero (static dispatch)
Flexibility Requires inheritance Duck typing
Documentation Separate from code Embedded in concept definition

Example Error Message Improvement

Before (Abstract Class)

error: no matching function for call to 'execute'
note: candidate function not viable: no known conversion from 
      'my_bad_executor' to 'IExecutor&' for 1st argument

After (Concepts)

error: constraints not satisfied for 'Executor<my_bad_executor>'
note: the expression 'executor.is_running()' is invalid
      because 'my_bad_executor' has no member named 'is_running'

Related Issues

  • kcenon/thread_system: Will benefit from Executor concept
  • kcenon/logger_system: Will benefit from Logger concept
  • kcenon/monitoring_system: Will benefit from MetricCollector concept

References

Metadata

Metadata

Assignees

Labels

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions