This commit is contained in:
deeaitch committed 2026-10-20 13:21:43 -04:00
1 parent e555ebb571
commit 849ce31e2f
17 files changed
+675 -35

No files matched your search

+53
View File
@@ -0,0 +1,53 @@
// SPDX-License-Identifier: MIT
#ifndef DBC_ATTRIBUTES_H
#define DBC_ATTRIBUTES_H
#include <cstdint>
#include <map>
#include <optional>
#include <string>
#include <variant>
#include <vector>
enum class AttributeScope {
Database,
Node,
Frame,
Signal,
Environment
};
enum class AttributeType {
Integer,
Hex,
Float,
String,
Enum
};
using AttributeValue = std::variant<std::int64_t, double, std::string>;
using AttributeMap = std::map<std::string, AttributeValue>;
struct AttributeDefinition {
AttributeScope scope = AttributeScope::Database;
AttributeType type = AttributeType::String;
std::int64_t integerMinimum = 0;
std::int64_t integerMaximum = 0;
double minimum = 0.0;
double maximum = 0.0;
std::vector<std::string> enumValues;
std::optional<AttributeValue> defaultValue;
};
/** Explicit assignment; defaults remain on definitions. Named nodes/environment
* variables are retained without validating declarations, which are not parsed. */
struct AttributeAssignment {
std::string name;
AttributeScope scope = AttributeScope::Database;
std::uint32_t canId = 0U;
bool isExtended = false;
std::string targetName;
AttributeValue value;
};
#endif
+2
View File
@@ -1,4 +1,5 @@
SOURCES += \
$$PWD/dbc_attributes.cpp \
$$PWD/decode_database.cpp \
$$PWD/dbc_decode_builder.cpp \
$$PWD/dbc_decoder.cpp \
@@ -8,6 +9,7 @@ SOURCES += \
$$PWD/tree_node.cpp
HEADERS += \
$$PWD/attributes.h \
$$PWD/j1939.h \
$$PWD/signal_layout.h \
$$PWD/definition_key.h \
+342
View File
@@ -0,0 +1,342 @@
// SPDX-License-Identifier: MIT
#include "dbc_statement_handler.h"
#include <charconv>
#include <cmath>
#include <limits>
#include <set>
namespace {
struct Token {
std::string text;
bool quoted = false;
};
std::vector<Token> Tokenize(const std::string &text) {
std::vector<Token> tokens;
std::size_t index = 0U;
while (index < text.size()) {
const char character = text[index++];
if (character == ' ' || character == '\t')
continue;
Token token;
if (character == '"') {
token.quoted = true;
bool closed = false;
while (index < text.size()) {
char next = text[index++];
if (next == '"') {
closed = true;
break;
}
if (next == '\\') {
if (index == text.size())
throw std::runtime_error("Incomplete attribute escape.");
next = text[index++];
if (next != '"' && next != '\\')
throw std::runtime_error("Invalid attribute escape.");
}
token.text += next;
}
if (!closed)
throw std::runtime_error("Unterminated attribute string.");
if (index < text.size() && text[index] != ' ' && text[index] != '\t' &&
text[index] != ',' && text[index] != ';')
throw std::runtime_error("Missing separator after attribute string.");
} else if (character == ';' || character == ',') {
token.text = character;
} else {
token.text = character;
while (index < text.size() && text[index] != ' ' && text[index] != '\t' &&
text[index] != ';' && text[index] != ',') {
if (text[index] == '"')
throw std::runtime_error("Missing separator before attribute string.");
token.text += text[index++];
}
}
tokens.push_back(token);
}
if (tokens.empty() || tokens.back().quoted || tokens.back().text != ";")
throw std::runtime_error("Attribute statement requires a semicolon.");
tokens.pop_back();
return tokens;
}
class AttributeTokens {
public:
explicit AttributeTokens(const std::string &line) : tokens(Tokenize(line)) {}
Token Take() {
if (position == tokens.size())
throw std::runtime_error("Incomplete attribute statement.");
return tokens[position++];
}
std::string Word() {
const Token token = Take();
if (token.quoted || token.text == "," || token.text == ";")
throw std::runtime_error("Expected an attribute keyword or number.");
return token.text;
}
std::string Quoted() {
const Token token = Take();
if (!token.quoted)
throw std::runtime_error("Expected a quoted attribute name or string.");
return token.text;
}
bool Empty() const { return position == tokens.size(); }
void End() const {
if (!Empty())
throw std::runtime_error("Unexpected trailing attribute fields.");
}
private:
std::vector<Token> tokens;
std::size_t position = 0U;
};
bool IsScope(const std::string &text) {
return text == "BO_" || text == "SG_" || text == "BU_" || text == "EV_";
}
AttributeScope ParseScope(const std::string &text) {
if (text == "BO_") return AttributeScope::Frame;
if (text == "SG_") return AttributeScope::Signal;
if (text == "BU_") return AttributeScope::Node;
if (text == "EV_") return AttributeScope::Environment;
throw std::runtime_error("Unsupported attribute scope: " + text);
}
std::int64_t Integer(const std::string &text) {
const char *begin = text.data();
const char *end = begin + text.size();
if (begin != end && *begin == '+') {
++begin;
if (begin != end && (*begin == '-' || *begin == '+'))
throw std::runtime_error("Invalid attribute number: " + text);
}
std::int64_t value = 0;
const std::from_chars_result parsed = std::from_chars(begin, end, value);
if (begin == end || parsed.ec != std::errc() || parsed.ptr != end)
throw std::runtime_error("Invalid attribute integer: " + text);
return value;
}
double Number(const std::string &text) {
const char *begin = text.data();
const char *end = begin + text.size();
if (begin != end && *begin == '+') {
++begin;
if (begin != end && (*begin == '-' || *begin == '+'))
throw std::runtime_error("Invalid attribute number: " + text);
}
double value = 0.0;
const std::from_chars_result parsed = std::from_chars(begin, end, value);
if (begin == end || parsed.ec != std::errc() || parsed.ptr != end || !std::isfinite(value))
throw std::runtime_error("Invalid attribute number: " + text);
return value;
}
AttributeValue Value(const Token &token, const AttributeDefinition &definition, bool isDefault) {
if (definition.type == AttributeType::String) {
if (!token.quoted) throw std::runtime_error("Attribute STRING requires quoted text.");
return token.text;
}
if (definition.type == AttributeType::Enum && isDefault && token.quoted) {
for (std::size_t index = 0U; index < definition.enumValues.size(); ++index) {
if (definition.enumValues[index] == token.text)
return static_cast<std::int64_t>(index);
}
throw std::runtime_error("Unknown attribute ENUM default label.");
}
if (token.quoted) throw std::runtime_error("Numeric attribute requires an unquoted number.");
if (definition.type == AttributeType::Float) {
const double value = Number(token.text);
if (value < definition.minimum || value > definition.maximum)
throw std::runtime_error("Attribute FLOAT value outside definition range.");
return value;
}
const std::int64_t value = Integer(token.text);
if (value < definition.integerMinimum || value > definition.integerMaximum)
throw std::runtime_error("Attribute integer/ENUM value outside definition range.");
return value;
}
void Define(AttributeTokens &tokens, DbcDatabase &database) {
AttributeDefinition definition;
const Token first = tokens.Take();
std::string name = first.text;
if (!first.quoted) {
definition.scope = ParseScope(first.text);
name = tokens.Quoted();
}
if (name.empty()) throw std::runtime_error("Empty attribute name.");
const std::string type = tokens.Word();
if (type == "INT" || type == "HEX") {
definition.type = type == "INT" ? AttributeType::Integer : AttributeType::Hex;
definition.integerMinimum = Integer(tokens.Word());
definition.integerMaximum = Integer(tokens.Word());
if (definition.integerMinimum > definition.integerMaximum)
throw std::runtime_error("Reversed attribute integer range.");
} else if (type == "FLOAT") {
definition.type = AttributeType::Float;
definition.minimum = Number(tokens.Word());
definition.maximum = Number(tokens.Word());
if (definition.minimum > definition.maximum)
throw std::runtime_error("Reversed attribute FLOAT range.");
} else if (type == "STRING") {
definition.type = AttributeType::String;
} else if (type == "ENUM") {
definition.type = AttributeType::Enum;
std::set<std::string> labels;
for (;;) {
const std::string label = tokens.Quoted();
if (!labels.insert(label).second)
throw std::runtime_error("Duplicate attribute ENUM label.");
definition.enumValues.push_back(label);
if (tokens.Empty()) break;
const Token separator = tokens.Take();
if (separator.quoted || separator.text != ",") throw std::runtime_error("Expected ENUM comma.");
}
definition.integerMaximum = static_cast<std::int64_t>(definition.enumValues.size() - 1U);
} else {
throw std::runtime_error("Unsupported attribute definition type: " + type);
}
tokens.End();
if (name == "VFrameFormat" && (definition.scope != AttributeScope::Frame || definition.type != AttributeType::Enum))
throw std::runtime_error("VFrameFormat requires a BO_ ENUM definition.");
if (name == "GenMsgCycleTime" && (definition.scope != AttributeScope::Frame ||
(definition.type != AttributeType::Integer && definition.type != AttributeType::Hex)))
throw std::runtime_error("GenMsgCycleTime requires a BO_ integer definition.");
if (!database.attributeDefinitions.emplace(name, definition).second)
throw std::runtime_error("Duplicate attribute definition: " + name);
}
AttributeDefinition &FindDefinition(DbcDatabase &database, const std::string &name) {
const std::map<std::string, AttributeDefinition>::iterator found = database.attributeDefinitions.find(name);
if (found == database.attributeDefinitions.end())
throw std::runtime_error("Undefined attribute: " + name);
return found->second;
}
void ParseId(const std::string &text, AttributeAssignment &assignment) {
const std::int64_t raw = Integer(text);
if (raw < 0 || raw > std::numeric_limits<std::uint32_t>::max() || (raw & 0x60000000U) != 0)
throw std::runtime_error("Invalid attribute CAN ID.");
const std::uint32_t id = static_cast<std::uint32_t>(raw);
assignment.canId = id & 0x1fffffffU;
assignment.isExtended = (id & 0x80000000U) != 0U || id > 0x7ffU;
}
AttributeMap &Target(DbcDatabase &database, const AttributeAssignment &assignment) {
if (assignment.scope == AttributeScope::Database) return database.attributes;
for (FrameInfo &frame: database.frames) {
if (frame.canId != assignment.canId || frame.isExtended != assignment.isExtended) continue;
if (assignment.scope == AttributeScope::Frame) return frame.attributes;
for (SignalInfo &signal: frame.signals) {
if (signal.name == assignment.targetName) return signal.attributes;
}
throw std::runtime_error("Unresolved attribute signal: " + assignment.targetName);
}
throw std::runtime_error("Unresolved attribute frame.");
}
void ValidateCycleTime(const std::string &name, const AttributeValue &value) {
if (name != "GenMsgCycleTime") return;
const std::int64_t milliseconds = std::get<std::int64_t>(value);
if (milliseconds < 0 || milliseconds > std::numeric_limits<std::uint32_t>::max())
throw std::runtime_error("GenMsgCycleTime must fit unsigned 32-bit milliseconds.");
}
void ApplyDefault(DbcDatabase &database, const std::string &name, const AttributeDefinition &definition) {
if (!definition.defaultValue) return;
if (definition.scope == AttributeScope::Database) database.attributes[name] = *definition.defaultValue;
for (FrameInfo &frame: database.frames) {
if (definition.scope == AttributeScope::Frame) frame.attributes[name] = *definition.defaultValue;
if (definition.scope == AttributeScope::Signal) {
for (SignalInfo &signal: frame.signals) signal.attributes[name] = *definition.defaultValue;
}
}
}
}// namespace
void DbcAttributeHandler::Handle(const std::string &line, DbcParseContext &context) const {
context.pendingAttributes.push_back({line, context.lineNumber});
}
void DbcParseContext::ResolveAttributes(const std::string &filePath, std::vector<DbcDiagnostic> &diagnostics) {
std::map<std::string, std::size_t> defaultLines;
std::vector<std::size_t> assignmentLines;
// Resolve each phase independently of source ordering.
const char *keywords[] = {"BA_DEF_", "BA_DEF_DEF_", "BA_"};
for (int phase = 0; phase < 3; ++phase) {
for (const PendingAttribute &pending: pendingAttributes) {
const std::string keyword = pending.statement.substr(0U, pending.statement.find_first_of(" \t"));
if (keyword != keywords[phase]) continue;
try {
AttributeTokens tokens(pending.statement);
tokens.Word();
if (phase == 0) {
Define(tokens, database);
continue;
}
const std::string name = tokens.Quoted();
AttributeDefinition &definition = FindDefinition(database, name);
if (phase == 1) {
if (definition.defaultValue) throw std::runtime_error("Duplicate attribute default: " + name);
definition.defaultValue = Value(tokens.Take(), definition, true);
ValidateCycleTime(name, *definition.defaultValue);
tokens.End();
defaultLines[name] = pending.lineNumber;
ApplyDefault(database, name, definition);
continue;
}
AttributeAssignment assignment;
assignment.name = name;
Token value = tokens.Take();
if (!value.quoted && IsScope(value.text)) {
assignment.scope = ParseScope(value.text);
if (assignment.scope == AttributeScope::Frame || assignment.scope == AttributeScope::Signal)
ParseId(tokens.Word(), assignment);
if (assignment.scope != AttributeScope::Frame)
assignment.targetName = tokens.Word();
value = tokens.Take();
}
if (assignment.scope != definition.scope) throw std::runtime_error("Attribute assignment scope differs from definition.");
assignment.value = Value(value, definition, false);
ValidateCycleTime(name, assignment.value);
tokens.End();
// Repeated explicit assignments use the last value, matching comments.
if (assignment.scope != AttributeScope::Node && assignment.scope != AttributeScope::Environment)
Target(database, assignment)[name] = assignment.value;
database.attributeAssignments.push_back(assignment);
assignmentLines.push_back(pending.lineNumber);
} catch (const std::exception &error) {
throw DbcDiagnosticError(DbcDiagnosticCode::InvalidAttribute, filePath, pending.lineNumber, error.what());
}
}
}
for (FrameInfo &frame: database.frames) {
for (const AttributeMap::value_type &attribute: frame.attributes) {
if (attribute.first != "VFrameFormat" && attribute.first != "GenMsgCycleTime") continue;
std::size_t line = defaultLines[attribute.first];
for (std::size_t index = 0U; index < database.attributeAssignments.size(); ++index) {
const AttributeAssignment &assignment = database.attributeAssignments[index];
if (assignment.name == attribute.first && assignment.scope == AttributeScope::Frame &&
assignment.canId == frame.canId && assignment.isExtended == frame.isExtended)
line = assignmentLines[index];
}
const std::int64_t value = std::get<std::int64_t>(attribute.second);
if (attribute.first == "GenMsgCycleTime") {
frame.cycleTimeMs = static_cast<std::uint32_t>(value);
continue;
}
frame.frameFormat = database.attributeDefinitions.at(attribute.first).enumValues[static_cast<std::size_t>(value)];
const std::string &format = *frame.frameFormat;
std::string warning;
if (format != "StandardCAN" && format != "ExtendedCAN")
warning = "Unsupported VFrameFormat '" + format + "' is preserved; decoding may be incomplete.";
else if ((format == "ExtendedCAN") != frame.isExtended)
warning = "VFrameFormat '" + format + "' conflicts with the BO_ CAN ID; ID interpretation is unchanged.";
if (!warning.empty())
diagnostics.push_back({DbcDiagnosticSeverity::Warning, DbcDiagnosticCode::UnsupportedAttribute, filePath, line, warning});
}
}
}
+3
View File
@@ -21,6 +21,9 @@
*/
struct DbcDatabase {
std::vector<FrameInfo> frames; /**< All frames found in the DBC file. */
std::map<std::string, AttributeDefinition> attributeDefinitions = {};
std::vector<AttributeAssignment> attributeAssignments = {};
AttributeMap attributes = {}; /**< Effective database attributes. */
};
#endif /* DBC_DATABASE_H */
+4
View File
@@ -19,6 +19,9 @@ DecodeDatabase DbcDecodeBuilder::Build(const DbcDatabase &source) const {
for (const FrameInfo &sourceFrame: source.frames) {
DecodeFrame targetFrame;
targetFrame.attributes = sourceFrame.attributes;
targetFrame.frameFormat = sourceFrame.frameFormat;
targetFrame.cycleTimeMs = sourceFrame.cycleTimeMs;
targetFrame.name = sourceFrame.name;
targetFrame.canId = sourceFrame.canId;
targetFrame.isExtended = sourceFrame.isExtended;
@@ -32,6 +35,7 @@ DecodeDatabase DbcDecodeBuilder::Build(const DbcDatabase &source) const {
for (const SignalInfo &sourceSignal: sourceFrame.signals) {
DecodeSignal targetSignal;
targetSignal.attributes = sourceSignal.attributes;
targetSignal.multiplexing = sourceSignal.multiplexing;
targetSignal.name = sourceSignal.name;
targetSignal.startBit = sourceSignal.startBit;
+5 -3
View File
@@ -19,14 +19,16 @@ enum class DbcDiagnosticCode {
InvalidStatement,
InvalidValueDescription,
InvalidMultiplexing,
UnsupportedStatement
UnsupportedStatement,
InvalidAttribute,
UnsupportedAttribute
};
struct DbcDiagnostic {
DbcDiagnosticSeverity severity;
DbcDiagnosticCode code;
std::string filePath;
std::size_t lineNumber; // One-based physical line; zero for file-level errors.
std::size_t lineNumber;// One-based physical line; zero for file-level errors.
std::string message;
};
@@ -35,7 +37,7 @@ struct DbcParseResult {
std::vector<DbcDiagnostic> diagnostics;
bool HasErrors() const {
for (const DbcDiagnostic &diagnostic : diagnostics) {
for (const DbcDiagnostic &diagnostic: diagnostics) {
if (diagnostic.severity == DbcDiagnosticSeverity::Error)
return true;
}
+1
View File
@@ -142,6 +142,7 @@ DbcParseResult DbcParser::ParseFileWithDiagnostics(const std::string &filePath)
context.ResolveComments(filePath);
context.ResolveValueDescriptions(filePath);
context.ValidateMultiplexing(filePath);
context.ResolveAttributes(filePath, result.diagnostics);
} catch (const DbcDiagnosticError &error) {
result.diagnostics.push_back(error.diagnostic);
return result;
+1
View File
@@ -31,6 +31,7 @@ struct DbcParserOptions {
* - CM_ SG_
* - basic M / mN multiplexing
* - VAL_ signal value descriptions
* - BA_DEF_, BA_DEF_DEF_, BA_ typed attributes, frame format and cycle time
*
* Frame/signal comments support multiline quoted text and deferred references.
*
+3
View File
@@ -385,6 +385,9 @@ DbcStatementDispatcher::DbcStatementDispatcher() {
RegisterType<DbcSignalHandler>("SG_");
RegisterType<DbcCommentHandler>("CM_");
RegisterType<DbcValueDescriptionHandler>("VAL_");
RegisterType<DbcAttributeHandler>("BA_DEF_");
RegisterType<DbcAttributeHandler>("BA_DEF_DEF_");
RegisterType<DbcAttributeHandler>("BA_");
}
void DbcHeaderHandler::Handle(const std::string &, DbcParseContext &) const {}
+12
View File
@@ -37,6 +37,11 @@ public:
DbcDiagnostic diagnostic;
};
struct PendingAttribute {
std::string statement;
std::size_t lineNumber;
};
/** Mutable state belonging to one file parse. Handlers do not retain it. */
struct DbcParseContext {
DbcDatabase &database;
@@ -51,6 +56,9 @@ struct DbcParseContext {
std::vector<PendingComment> pendingComments = {};
void ResolveComments(const std::string &filePath);
std::vector<PendingAttribute> pendingAttributes = {};
void ResolveAttributes(const std::string &filePath, std::vector<DbcDiagnostic> &diagnostics);
void ResolveValueDescriptions(const std::string &filePath);
};
@@ -79,7 +87,11 @@ private:
class DbcCommentHandler : public DbcStatementHandler {
public:
void Handle(const std::string &line, DbcParseContext &context) const override;
};
class DbcAttributeHandler : public DbcStatementHandler {
public:
void Handle(const std::string &line, DbcParseContext &context) const override;
};
class DbcValueDescriptionHandler : public DbcStatementHandler {
+7
View File
@@ -12,6 +12,7 @@
#ifndef DECODE_DATABASE_H
#define DECODE_DATABASE_H
#include "attributes.h"
#include "definition_key.h"
#include "multiplexing.h"
#include "value_descriptions.h"
@@ -56,6 +57,8 @@ struct DecodeSignal {
ValueDescriptions valueDescriptions; /**< Raw integer state descriptions. */
std::string comment; /**< Comment. */
AttributeMap attributes;
DecodeSignal() = default;
};
@@ -73,6 +76,10 @@ struct DecodeFrame {
std::string comment; /**< Frame comment. */
std::vector<DecodeSignal> signals; /**< Signal definitions. */
AttributeMap attributes;
std::optional<std::string> frameFormat;
std::optional<std::uint32_t> cycleTimeMs;
DecodeFrame() = default;
FrameKey GetKey() const {
+15 -9
View File
@@ -12,6 +12,7 @@
#ifndef FRAME_INFO_H
#define FRAME_INFO_H
#include "attributes.h"
#include <cstdint>
#include <string>
#include <vector>
@@ -22,15 +23,20 @@
* @brief Describes one CAN frame from a DBC file.
*/
struct FrameInfo {
std::string name; /**< Frame name. */
std::uint32_t canId = 0U; /**< Normalized CAN identifier. */
bool isExtended = false; /**< true for extended frame. */
std::uint32_t pgn = 0U; /**< J1939 PGN if applicable. */
bool hasPgn = false; /**< Explicit J1939 opt-in; pgn is derived from canId. */
std::uint8_t dlc = 0U; /**< Frame payload length. */
std::string transmitter; /**< Transmitter ECU name. */
std::string comment; /**< Optional frame comment. */
std::vector<SignalInfo> signals; /**< Signals contained in the frame. */
std::string name; /**< Frame name. */
std::uint32_t canId = 0U; /**< Normalized CAN identifier. */
bool isExtended = false; /**< true for extended frame. */
std::uint32_t pgn = 0U; /**< J1939 PGN if applicable. */
bool hasPgn = false; /**< Explicit J1939 opt-in; pgn is derived from canId. */
std::uint8_t dlc = 0U; /**< Frame payload length. */
std::string transmitter; /**< Transmitter ECU name. */
std::string comment; /**< Optional frame comment. */
std::vector<SignalInfo> signals; /**< Signals contained in the frame. */
std::optional<std::string> frameFormat; /**< VFrameFormat enum label. */
std::optional<std::uint32_t> cycleTimeMs; /**< GenMsgCycleTime; zero means unspecified. */
AttributeMap attributes; /**< Effective values, including defaults. */
FrameInfo() = default;
};
+17 -14
View File
@@ -12,8 +12,9 @@
#ifndef SIGNAL_INFO_H
#define SIGNAL_INFO_H
#include "value_descriptions.h"
#include "attributes.h"
#include "multiplexing.h"
#include "value_descriptions.h"
#include <cstdint>
#include <string>
#include <vector>
@@ -23,19 +24,21 @@
*/
struct SignalInfo {
Multiplexing multiplexing;
std::string name; /**< Signal name. */
std::uint32_t startBit = 0U; /**< Start bit in DBC notation. */
std::uint32_t length = 0U; /**< Signal length in bits. */
bool isLittleEndian = true; /**< true for Intel, false for Motorola. */
bool isSigned = false; /**< true if signal is signed. */
double factor = 1.0; /**< Scaling factor. */
double offset = 0.0; /**< Physical offset. */
double minimum = 0.0; /**< Minimum physical value. */
double maximum = 0.0; /**< Maximum physical value. */
std::string unit; /**< Physical unit. */
std::vector<std::string> receivers; /**< Signal receivers. */
ValueDescriptions valueDescriptions; /**< Raw integer state descriptions. */
std::string comment; /**< Optional signal comment. */
std::string name; /**< Signal name. */
std::uint32_t startBit = 0U; /**< Start bit in DBC notation. */
std::uint32_t length = 0U; /**< Signal length in bits. */
bool isLittleEndian = true; /**< true for Intel, false for Motorola. */
bool isSigned = false; /**< true if signal is signed. */
double factor = 1.0; /**< Scaling factor. */
double offset = 0.0; /**< Physical offset. */
double minimum = 0.0; /**< Minimum physical value. */
double maximum = 0.0; /**< Maximum physical value. */
std::string unit; /**< Physical unit. */
std::vector<std::string> receivers; /**< Signal receivers. */
ValueDescriptions valueDescriptions; /**< Raw integer state descriptions. */
std::string comment; /**< Optional signal comment. */
AttributeMap attributes; /**< Effective values, including defaults. */
SignalInfo() = default;
};