24 KiB
Component Inheritance and Casting Strategy
1. The Problem
The Loadcell component requires functionality from both RTU_Base (for RS485 communication) and NetworkComponent (for Modbus TCP exposure). Both of these base classes inherit from Component.
This creates a "diamond inheritance" pattern:
Component
/ \
RTU_Base NetworkComponent
\ /
Loadcell
This leads to two critical issues in C++:
- Ambiguous Base Class: Any code trying to access members of
Componentthrough aLoadcellpointer (e.g.,loadcell->id) is ambiguous. Does it accessRTU_Base::ComponentorNetworkComponent::Component? - Casting Failures: The standard solution to ambiguity is
virtualinheritance. However, ifComponentis a virtual base, downcasting from aComponent*to a derived class pointer (e.g.,OmronE5*) usingstatic_castbecomes illegal. Since the project does not use RTTI,dynamic_castis not available as an alternative.
This document outlines the viable and non-viable options to resolve this.
2. Option A: Virtual Inheritance (Non-Viable)
This is the classic C++ solution for the diamond problem.
-
Concept: Have
RTU_BaseandNetworkComponentinheritvirtual public Component. This ensures thatLoadcellreceives only one shared instance of theComponentbase class, resolving the ambiguity. -
Why it Fails: This approach makes it impossible to safely downcast. Code elsewhere in the project iterates through a list of
Component*and casts them to specific types likeOmronE5*.// This becomes illegal if Component is a virtual base OmronE5* omron = static_cast<OmronE5*>(component_ptr);The C++ standard forbids
static_castfor downcasting from a virtual base because the memory layout is not known at compile time. The only standard-compliant tool for this isdynamic_cast, which requires RTTI. -
Conclusion: Due to the hard requirement of downcasting
Componentpointers without RTTI, this option is not feasible.
3. Option B: Composition over Inheritance (Recommended)
This option redesigns the relationship to avoid multiple inheritance of Component.
-
Concept: Instead of
Loadcell"being a"NetworkComponent, it will "have a" network helper. We will refactorNetworkComponentinto a helper class that does not inherit fromComponent.Loadcellwill continue to inherit fromRTU_Base(and thusComponent), but will contain the new network helper as a member variable. -
Implementation Steps:
- Create a new template class,
NetworkHelper<N>, from the code inNetworkComponent. NetworkHelperwill not inherit fromComponent.- The constructor for
NetworkHelperwill take aComponent* ownerto get access toid,name,slaveId, etc. - Modify
Loadcellto have a member variable:NetworkHelper<6> _netHelper;. - The
Loadcellconstructor will initialize_netHelper, passingthisas the owner. Loadcellwill implement network-related virtual functions (likemb_tcp_blocks) by forwarding the calls to its_netHelpermember.- The
INIT_NETWORK_VALUEmacro must be updated to work with this new structure, or a new macro must be created.
- Create a new template class,
-
Pros:
- Solves the Problem: Completely avoids the diamond inheritance and eliminates all ambiguity and casting issues.
- Clear Relationship: Arguably, a component "having" network capabilities is a clearer design than it "being" a network interface.
- Safe: No unsafe casting or compiler-specific tricks are needed.
-
Cons:
- Refactoring Effort:
NetworkComponentmust be refactored. Any other components that currently inherit from it (likeNetworkValueTest) will also need to be updated to use the newNetworkHelpervia composition. - Boilerplate Code: Components like
Loadcellwill need to explicitly forward calls to their helper member, adding a small amount of boilerplate code.
- Refactoring Effort:
4. Option C: Component-centric Feature System
This is a more advanced alternative that moves the "feature" concept from NetworkValue directly into the Component base class.
-
Concept: Instead of creating helper classes or using multiple inheritance, the
Componentclass itself would be designed to be composed of optional features. Any component could be configured to include capabilities likeModbus,ValueNotify, etc. -
Implementation Sketch:
- Generalize Feature Classes: The
NV_Modbus,NV_Logging, andNV_Notifyclasses would be refactored into generic, standalone "Feature" classes that do not inherit fromComponent. - Adapt
Component: TheComponentclass would be modified to aggregate these features. This could be done in two ways:- Compile-Time (Templates):
Componentcould become a variadic template class that inherits from the features it's given. This is powerful but creates a cascade of templates through all inheriting classes (RTU_Base,Loadcell, etc.), significantly increasing complexity.template<typename... Features> class Component : public Features... { /* ... */ }; // Usage would be complex: class Loadcell : public RTU_Base<Feature_Modbus, ...> {}; - Runtime (Composition):
Componentcould hold pointers to optional feature objects, allocated on the heap. This avoids the template complexity but introduces dynamic memory management.class Component { std::vector<IFeature*> _features; public: template<typename F, typename... Args> void addFeature(Args&&... args) { _features.push_back(new F(std::forward<Args>(args)...)); } };
- Compile-Time (Templates):
- Update
Loadcell:Loadcellwould inherit fromRTU_Baseas usual. In its constructor, it would callthis->addFeature<Feature_Modbus>(...)andthis->addFeature<Feature_ValueNotify<uint32_t>>(...)to gain the required functionality.
- Generalize Feature Classes: The
-
Pros:
- Ultimate Flexibility: Provides a unified way to add any capability to any component.
- Clean Hierarchy: Completely avoids all inheritance problems.
Loadcellhas a clear, single inheritance path fromComponentviaRTU_Base. - No Boilerplate Forwarding: The
Componentbase class could manage callingloop()orsetup()on all its registered features automatically.
-
Cons:
- Highest Refactoring Effort: This is a fundamental change to the core
Componentclass and would require updating every component in the system. - Architectural Complexity: Designing a robust and easy-to-use feature system is a significant undertaking. The choice between compile-time templates and runtime pointers has major implications for code complexity and memory usage.
- Highest Refactoring Effort: This is a fundamental change to the core
5. Option D: Targeted Static Casting (Minimalist Fix)
This option resolves the ambiguity only at the specific lines of code where the compiler errors occur, without changing any class definitions.
-
Concept: Use
static_castto explicitly tell the compiler which inheritance path to take when converting aLoadcell*to aComponent*or when accessing a member ofComponent. We can choose eitherRTU_BaseorNetworkComponentas the intermediate path. SinceRTU_Baseis the primary functional parent, it's the more logical choice. -
Implementation Steps:
- Fixing
components.push_back(loadCell_0): Insrc/PHApp.cpp, cast theloadCell_0pointer to anRTU_Base*before adding it to the vector. This resolves the ambiguity of whichComponentbase to use.// src/PHApp.cpp components.push_back(static_cast<RTU_Base*>(phApp->loadCell_0)); - Fixing
loadCell_0->owner = rs485: Insrc/RS485Devices.cpp, cast the pointer toRTU_Base*before accessing theownermember.// src/RS485Devices.cpp static_cast<RTU_Base*>(phApp->loadCell_0)->owner = rs485;
- Fixing
-
Pros:
- Minimal Change: Requires modifying only the two lines of code that produce compiler errors. It is by far the least invasive solution.
- No Class Refactoring: Avoids any changes to
Component,RTU_Base,NetworkComponent, orLoadcell. - No Performance Overhead:
static_castis a compile-time operation with zero runtime cost.
-
Cons:
- Brittle & Localized: This is a tactical patch, not a strategic architectural solution. It fixes these two errors but doesn't solve the underlying diamond inheritance problem. If
Loadcellis used in any other ambiguous context in the future, that new code will also fail to compile and will require a similar explicit cast. - Technical Debt: This approach knowingly papers over a design flaw. It can make the code harder to reason about for future developers who will have to understand why this specific component needs special casting.
- Brittle & Localized: This is a tactical patch, not a strategic architectural solution. It fixes these two errors but doesn't solve the underlying diamond inheritance problem. If
6. Option E: Virtual Inheritance + Manual RTTI (LLVM-style dyn_cast)
This option revisits virtual inheritance (Option A) and solves its downcasting problem by implementing a manual, type-safe casting function.
-
Concept: The
Componentbase class already has atypemember that serves as a manual run-time type identifier. We can use this to create adyn_castfunction that checks this type before performing a safestatic_cast. This avoids the need for the compiler's RTTI anddynamic_cast. -
Implementation Steps:
-
Enable Virtual Inheritance: First, implement Option A.
RTU_BaseandNetworkComponentmust inheritvirtual public Component. This solves the diamond inheritance ambiguity. -
Ensure Type ID is Set: Every class that inherits from
Componentmust set itstypemember correctly in its constructor (e.g.,type = COMPONENT_TYPE::COMPONENT_TYPE_LOADCELL;). -
Implement
dyn_cast: Create adyn_casttemplate function that checks the component's type before casting.// in a new component_cast.h template<typename To, typename From> To* dyn_cast(From* src) { using T = std::remove_pointer_t<To>; if (!src) return nullptr; if (src->type == T::COMPONENT_TYPE_ENUM) { return static_cast<To*>(src); } return nullptr; } // Note: This requires each castable class (e.g., OmronE5) // to expose its type enum, for example: // class OmronE5 : public RTU_Base { // public: // static const COMPONENT_TYPE COMPONENT_TYPE_ENUM = COMPONENT_TYPE::COMPONENT_TYPE_PID; // OmronE5() { type = COMPONENT_TYPE_ENUM; } // ... // }; -
Replace Casts: Replace the
static_castcalls that were failing insrc/PHApp.cppwith the newdyn_cast.// src/PHApp.cpp if (auto* omron = dyn_cast<OmronE5>(comp)) { omron->setConsumption(...); }
-
-
Pros:
- "Correct" Inheritance: Allows for a true "is-a" relationship using standard C++ virtual inheritance, which correctly models the single
Componentbase. - Type Safe: The
dyn_castcheck prevents incorrect casts at runtime. - Centralized Logic: The casting logic is contained within the
dyn_castfunction, not scattered across the application.
- "Correct" Inheritance: Allows for a true "is-a" relationship using standard C++ virtual inheritance, which correctly models the single
-
Cons:
- Requires Discipline: Developers must remember to set the
typeandCOMPONENT_TYPE_ENUMfor every new component. - Two-Step Refactor: Requires implementing virtual inheritance first and then fixing the resulting casting issues. This is more involved than the targeted patch of Option D.
slaveIdAmbiguity: TheslaveIdmember exists in bothComponentandRTU_Base. Virtual inheritance will not solve this ambiguity; it must be resolved manually (e.g., by removing it from one of the classes or renaming it).
- Requires Discipline: Developers must remember to set the
7. Recommendation
Option B (Composition over Inheritance) remains the most robust long-term solution. It correctly models the relationships between the classes ("has-a" vs "is-a") and eliminates the root cause of all ambiguity problems without manual workarounds.
However, if maintaining the "is-a" relationship with multiple inheritance is a design goal, then Option E (Virtual Inheritance + dyn_cast) is the most architecturally sound way to achieve it. It is superior to the simple static_cast patches of Option D because it correctly solves the underlying single-base-instance problem first, and then provides a safe way to handle the consequences.
Updated Recommendation:
- For a quick, low-impact fix that accepts some technical debt: Choose Option D.
- For the most robust, unambiguous, and maintainable architecture: Choose Option B.
- For a "correct" multiple inheritance architecture: Choose Option E. It is a valid but more involved alternative to Option B.
8. Option G: Refactoring RTU_Base into a Feature Mixin
This section outlines the strategy for refactoring RTU_Base into a NC_Rtu feature mixin for NetworkComponent. This will create a unified base class for all Modbus components, solving the diamond inheritance problem. The documentation will cover the concept, implementation details, a refactoring example using OmronE5, and a thorough analysis of the pros and cons of this advanced architectural approach.
8.1. Concept: A Single, Powerful Base Class
The core idea is to treat "being a Modbus RTU client" not as a base class to inherit from, but as a capability or feature that a NetworkComponent can possess. This elevates NetworkComponent to be the single, unified base for any component that needs to communicate over Modbus, whether it's acting as a TCP server endpoint, an RTU client, or both.
8.2. How It Would Work: The NC_Rtu Feature
We would introduce a new feature class, analogous to NV_Logging or NV_Modbus from NetworkValue, but for NetworkComponent itself.
-
Create
NC_Rtu: A new feature class,NC_Rtu, would be created. This class would not inherit fromComponent. It would contain the logic currently found inRTU_Base, such as:- The
addMandatoryReadBlock()method. - The logic to queue write operations with the global
ModbusRTUmanager. - State tracking for RTU communication (timeouts, errors, etc.).
- The
-
Enhance
NetworkComponent:NetworkComponentwould be modified to optionally inherit fromNC_Rtubased on a template parameter or feature flag.- It would gain the
onRegisterUpdate(uint16_t address, uint16_t newValue)virtual function, which would be called by theNC_Rtufeature when new data arrives from the bus. - The
NetworkComponentwould manage a pointer to the globalModbusRTUinstance, passing it to theNC_Rtufeature upon initialization.
- It would gain the
8.3. Blueprint: Refactoring OmronE5
The OmronE5 component provides a clear "after" picture for this architecture.
Current (Problematic) Structure:
// Inherits from RTU_Base, which creates a diamond with NetworkComponent
class OmronE5 : public RTU_Base { /* ... */ };
Proposed Unified Structure:
// A template parameter could enable the RTU feature
template <size_t N, bool HasRtu = false>
class NetworkComponent : public Component, public maybe<HasRtu, NC_Rtu>
{ /* ... */ };
// OmronE5 inherits from ONE base class and declares its needs
class OmronE5 : public NetworkComponent<OMRON_TCP_BLOCK_COUNT, /* HasRtu = */ true>
{
public:
OmronE5(Component* owner, uint8_t slaveId, ...)
: NetworkComponent(owner, ...)
{
// Initialize the RTU feature with its slaveId
this->initRtuFeature(slaveId);
}
short setup() override {
// TCP setup is handled by NetworkComponent's base setup
NetworkComponent::setup();
// Add RTU read blocks. This method now comes from the NC_Rtu mixin.
this->addMandatoryReadBlock(0x0000, 6, FN_READ_HOLD_REGISTER);
// ... other setup ...
return E_OK;
}
// This gets called by the NC_Rtu feature when data arrives from the bus
void onRegisterUpdate(uint16_t address, uint16_t newValue) override {
// Handle RTU register updates here...
}
// TCP methods remain unchanged
short mb_tcp_read(MB_Registers* reg) override { /* ... */ }
short mb_tcp_write(MB_Registers* reg, short value) override { /* ... */ }
uint16_t mb_tcp_base_address() const override { /* ... */ }
};
8.4. Analysis of the Unified Approach
This model represents a highly integrated and powerful design.
Pros:
- Solves Diamond Inheritance: It completely eliminates the diamond inheritance problem by establishing a single, clear inheritance path for all networked components.
- Reduces Boilerplate: Components like
OmronE5andLoadcellbecome much simpler. They no longer need to manually implement forwarding to a helper class; they simply inherit the required functionality from their singleNetworkComponentbase. - True "Is-A" Relationship: A component truly "is a"
NetworkComponent, with all the capabilities that entails, rather than having to delegate to helper objects.
Cons:
- "God Class" Risk: This approach concentrates a significant amount of responsibility into
NetworkComponent, making it a large and complex class that handles both TCP server logic and RTU client logic. This can make the base class harder to understand and maintain. - High Refactoring Cost: This is the most invasive refactoring option. It requires fundamentally redesigning
NetworkComponentandRTU_Base, and would necessitate updating every single component that currently uses either of them. - Blurred Responsibilities: It merges the very different concerns of being a passive TCP endpoint and an active RTU client into a single class, which can be seen as a violation of the Single Responsibility Principle.
8.5. Conclusion on Option F
Option F is an architecturally elegant but pragmatically challenging solution. It offers the cleanest public API for components like OmronE5 and Loadcell, but at the cost of creating a highly complex base class and requiring a system-wide refactoring effort. It should be considered a long-term architectural goal, to be approached with caution after the more immediate problems are solved using the less invasive compositional approach (Option B).
9. Final Architecture: A Unified NetworkComponent with Composable Features
This section details the definitive architectural solution that resolves the diamond inheritance problem by refactoring NetworkComponent into a versatile, feature-based class. This approach draws inspiration from the design of NetworkValue, using template-based composition to mix in required capabilities like Modbus TCP and RTU, rather than relying on multiple inheritance or helper objects.
9.1. Core Concept: From Inheritance to Feature Composition
The fundamental shift is to stop treating network protocols as base classes to inherit from. Instead, they become features that are composed at compile time into a single, lean NetworkComponent base.
- A Lean
NetworkComponentBase: TheNetworkComponentclass will be refactored into a variadic template class. It will inherit fromComponentand also from any feature classes passed to it as template arguments. - Standalone Feature Classes (
NC_ModbusTcp,NC_ModbusRtu):- The logic from the current
NetworkComponentwill be extracted into a feature class namedNC_ModbusTcp. - The logic from
RTU_Basewill be extracted into a feature class namedNC_ModbusRtu. - Crucially, these feature classes will not inherit from
Component. They are pure feature mixins. They will be given an owner pointer to theNetworkComponentinstance to access shared data (id,name, etc.).
- The logic from the current
9.2. Architecture Diagram
This diagram illustrates the final proposed architecture. NetworkComponent acts as an aggregator, composing the TCP and RTU features. Loadcell then inherits this aggregated functionality.
classDiagram
direction LR
Component <|-- NetworkComponent
NetworkComponent <|-- Loadcell
class Component {
+id
+name
+owner
+enabled()
}
class NetworkComponent {
<<template (T...)>>
+mb_tcp_register()
+onRegisterUpdate()
}
NetworkComponent --o NC_ModbusTcp : composes
NetworkComponent --o NC_ModbusRtu : composes
class NC_ModbusTcp {
<<feature>>
#registerBlock()
#mb_tcp_read()
#mb_tcp_write()
}
class NC_ModbusRtu {
<<feature>>
#addMandatoryReadBlock()
#onRegisterUpdate()
}
class Loadcell {
+getWeight()
}
9.3. Blueprint: Refactoring Loadcell
This code demonstrates how Loadcell would be implemented under the new architecture.
-
Refactored
NetworkComponentHeader (Conceptual):// In a new file, e.g., features/NC_ModbusTcp.h class NC_ModbusTcp { /* ... TCP logic ... */ }; // In a new file, e.g., features/NC_ModbusRtu.h class NC_ModbusRtu { /* ... RTU logic ... */ }; // The new NetworkComponent.h #include "features/NC_ModbusTcp.h" #include "features/NC_ModbusRtu.h" template <typename... Features> class NetworkComponent : public Component, public Features... { public: NetworkComponent(Component* owner, ...) : Component(owner, ...) { // Pass 'this' pointer to each feature (Features::initFeature(this), ...); } // ... common network component logic ... }; -
LoadcellImplementation: TheLoadcellclass becomes significantly cleaner. It declares its needs through the template arguments of its base class.// In Loadcell.h #include "modbus/NetworkComponent.h" #include "features/NC_ModbusTcp.h" #include "features/NC_ModbusRtu.h" class Loadcell : public NetworkComponent<NC_ModbusTcp, NC_ModbusRtu> { public: Loadcell(Component* owner, uint8_t slaveId, ...); short setup() override; short loop() override; // This is an RTU-specific callback, now part of the class interface void onRegisterUpdate(uint16_t address, uint16_t newValue) override; // These are TCP-specific methods, also part of the class interface short mb_tcp_read(MB_Registers* reg) override; short mb_tcp_write(MB_Registers* reg, short value) override; uint16_t mb_tcp_base_address() const override; // ... other Loadcell methods }; // In Loadcell.cpp Loadcell::Loadcell(Component* owner, uint8_t slaveId, ...) : NetworkComponent(owner, ...) // Base class constructor { // Feature constructors (if any) are handled by NetworkComponent this->setRtuSlaveId(slaveId); // Method inherited from NC_ModbusRtu feature } short Loadcell::setup() { NetworkComponent::setup(); // Calls setup on all features // RTU setup addMandatoryReadBlock(...); // TCP setup registerBlock(...); return E_OK; }
9.4. Final Recommendation
This unified, feature-based composition model is the recommended architectural direction. It is the most robust, flexible, and scalable solution, completely eliminating the diamond inheritance problem while providing a clean and expressive API for component development. While it represents a significant refactoring effort, the long-term benefits to code clarity, maintainability, and extensibility are substantial.