stabilized logger classes

This commit is contained in:
Sascha Kühl 2026-02-04 14:45:26 +01:00
parent e7e66f44e2
commit 520b2bab49
10 changed files with 112 additions and 135 deletions

View File

@ -62,7 +62,7 @@ using namespace work::models;
// payload.is_polymorphic_type<jobs::IdPayload>(); // payload.is_polymorphic_type<jobs::IdPayload>();
int main() { int main() {
logger::default_min_log_level(logger::log_level::LVL_DEBUG); logger::default_min_log_level(logger::log_level::Debug);
logger::add_log_sink(logger::create_stdout_sink()); logger::add_log_sink(logger::create_stdout_sink());
// sql::connection_pool<sql::connection> pool("postgres://news:news@127.0.0.1:15432/matador", 4); // sql::connection_pool<sql::connection> pool("postgres://news:news@127.0.0.1:15432/matador", 4);

View File

@ -101,7 +101,7 @@ public:
void clear(); void clear();
private: private:
void get_time_stamp(char* timestamp_buffer) const; static void get_time_stamp(char* timestamp_buffer);
private: private:
static std::map<log_level, std::string> level_strings; static std::map<log_level, std::string> level_strings;

View File

@ -8,15 +8,14 @@ namespace matador::logger {
/** /**
* Represents all available log levels * Represents all available log levels
*/ */
enum class log_level enum class log_level {
{ Fatal, /**< If a serious error occurred, use FATAL level */
LVL_FATAL, /**< If a serious error occurred, use FATAL level */ Error, /**< On error use ERROR level */
LVL_ERROR, /**< On error use ERROR level */ Warn, /**< Warnings should use WARN level */
LVL_WARN, /**< Warnings should use WARN level */ Info, /**< Information should go with INFO level */
LVL_INFO, /**< Information should go with INFO level */ Debug, /**< Debug output should use DEBUG level */
LVL_DEBUG, /**< Debug output should use DEBUG level */ Trace, /**< Trace information should use TRACE level */
LVL_TRACE, /**< Trace information should use TRACE level */ All /**< This level represents all log levels and should be used for logging */
LVL_ALL /**< This level represents all log levels and should be used for logging */
}; };
/** /**
@ -31,10 +30,9 @@ std::ostream& operator<<(std::ostream &os, log_level lvl);
/// @cond MATADOR_DEV /// @cond MATADOR_DEV
struct log_level_range struct log_level_range {
{ log_level min_level = log_level::Info;
log_level min_level = log_level::LVL_INFO; log_level max_level = log_level::Fatal;
log_level max_level = log_level::LVL_FATAL;
}; };
/// @endcond /// @endcond

View File

@ -123,10 +123,7 @@ public:
protected: protected:
/// @cond MATADOR_DEV /// @cond MATADOR_DEV
log_manager() log_manager();
{
default_log_domain_ = log_domain_map_.insert(std::make_pair("default", std::make_shared<log_domain>("default", default_log_level_range_))).first->second;
}
/// @endcond /// @endcond
private: private:
@ -140,6 +137,8 @@ private:
std::map<std::string, std::shared_ptr<log_domain>> log_domain_map_; std::map<std::string, std::shared_ptr<log_domain>> log_domain_map_;
static log_level_range default_log_level_range_; static log_level_range default_log_level_range_;
std::mutex mutex_;
}; };
/** /**
@ -285,15 +284,10 @@ void log_default(log_level lvl, const std::string &source, const char *message);
* @param args The arguments for the message * @param args The arguments for the message
*/ */
template<typename... ARGS> template<typename... ARGS>
void log(const log_level lvl, const std::string &source, const char *what, ARGS const &... args) void log(const log_level lvl, const std::string &source, const char *what, ARGS const &... args) {
{ char message_buffer[logger::buffer_size];
char message_buffer[16384];
#ifdef _MSC_VER snprintf(message_buffer, logger::buffer_size, what, args...);
sprintf_s(message_buffer, 912, what, args...);
#else
sprintf(message_buffer, what, args...);
#endif
log_default(lvl, source, message_buffer); log_default(lvl, source, message_buffer);
} }

View File

@ -9,7 +9,6 @@
#include <mutex> #include <mutex>
namespace matador::logger { namespace matador::logger {
/** /**
* @brief logger to write log messages to log domains * @brief logger to write log messages to log domains
* *
@ -31,9 +30,9 @@ namespace matador::logger {
* All log messages are written through the internal * All log messages are written through the internal
* log_domain object to the sinks. * log_domain object to the sinks.
*/ */
class logger final class logger final {
{
public: public:
static constexpr size_t buffer_size = 16384;
/** /**
* Create a logger with a given source name connected * Create a logger with a given source name connected
@ -69,7 +68,7 @@ public:
* @param args The arguments to be replaced in the message * @param args The arguments to be replaced in the message
*/ */
template<typename ... ARGS> template<typename ... ARGS>
void fatal(const char *what, ARGS const &... args) const { log(log_level::LVL_FATAL, what, args...); } void fatal(const char *what, ARGS const &... args) const { log(log_level::Fatal, what, args...); }
/** /**
* Writes a log message string with log level LVL_FATAL * Writes a log message string with log level LVL_FATAL
@ -91,7 +90,7 @@ public:
* @param args The arguments to be replaced in the message * @param args The arguments to be replaced in the message
*/ */
template<typename ... ARGS> template<typename ... ARGS>
void error(const char *what, ARGS const &... args) const { log(log_level::LVL_ERROR, what, args...); } void error(const char *what, ARGS const &... args) const { log(log_level::Error, what, args...); }
/** /**
* Writes a log message string with log level LVL_FATAL * Writes a log message string with log level LVL_FATAL
@ -113,7 +112,7 @@ public:
* @param args The arguments to be replaced in the message * @param args The arguments to be replaced in the message
*/ */
template<typename ... ARGS> template<typename ... ARGS>
void warn(const char *what, ARGS const &... args) const { log(log_level::LVL_WARN, what, args...); } void warn(const char *what, ARGS const &... args) const { log(log_level::Warn, what, args...); }
/** /**
* Writes a log message string with log level LVL_FATAL * Writes a log message string with log level LVL_FATAL
@ -135,7 +134,7 @@ public:
* @param args The arguments to be replaced in the message * @param args The arguments to be replaced in the message
*/ */
template<typename ... ARGS> template<typename ... ARGS>
void info(const char *what, ARGS const &... args) const { log(log_level::LVL_INFO, what, args...); } void info(const char *what, ARGS const &... args) const { log(log_level::Info, what, args...); }
/** /**
* Writes a log message string with log level LVL_FATAL * Writes a log message string with log level LVL_FATAL
@ -157,7 +156,7 @@ public:
* @param args The arguments to be replaced in the message * @param args The arguments to be replaced in the message
*/ */
template<typename ... ARGS> template<typename ... ARGS>
void debug(const char *what, ARGS const &... args) const { log(log_level::LVL_DEBUG, what, args...); } void debug(const char *what, ARGS const &... args) const { log(log_level::Debug, what, args...); }
/** /**
* Writes a log message string with log level LVL_FATAL * Writes a log message string with log level LVL_FATAL
@ -179,7 +178,7 @@ public:
* @param args The arguments to be replaced in the message * @param args The arguments to be replaced in the message
*/ */
template<typename ... ARGS> template<typename ... ARGS>
void trace(const char *what, ARGS const &... args) const { log(log_level::LVL_TRACE, what, args...); } void trace(const char *what, ARGS const &... args) const { log(log_level::Trace, what, args...); }
/** /**
* Writes a log message represented by a char pointer * Writes a log message represented by a char pointer
@ -222,14 +221,10 @@ private:
}; };
template<typename... ARGS> template<typename... ARGS>
void logger::log(log_level lvl, const char *what, ARGS const &... args) const { void logger::log(const log_level lvl, const char *what, ARGS const &... args) const {
char message_buffer[16384]; char message_buffer[buffer_size];
#ifdef _MSC_VER snprintf(message_buffer, buffer_size, what, args...);
sprintf_s(message_buffer, 16384, what, args...);
#else
sprintf(message_buffer, what, args...);
#endif
logger_domain_->log(lvl, source_, message_buffer); logger_domain_->log(lvl, source_, message_buffer);
} }

View File

@ -44,11 +44,13 @@ char* gettimestamp(char* const buffer, const size_t size) {
} }
std::map<log_level, std::string> log_domain::level_strings = { /* NOLINT */ std::map<log_level, std::string> log_domain::level_strings = { /* NOLINT */
{ log_level::LVL_DEBUG, "DEBUG" }, { log_level::Fatal, "Fatal" },
{ log_level::LVL_INFO, "INFO" }, { log_level::Debug, "Debug" },
{ log_level::LVL_WARN, "WARN" }, { log_level::Info, "Info" },
{ log_level::LVL_ERROR, "ERROR" }, { log_level::Warn, "Warn" },
{ log_level::LVL_TRACE, "TRACE" } { log_level::Error, "Error" },
{ log_level::Trace, "Trace" },
{ log_level::All, "All" }
}; };
log_domain::log_domain(std::string name, const log_level_range log_range) log_domain::log_domain(std::string name, const log_level_range log_range)
@ -61,33 +63,29 @@ std::string log_domain::name() const
return name_; return name_;
} }
void log_domain::max_log_level(log_level max_level) void log_domain::max_log_level(const log_level max_level) {
{
log_level_range_.max_level = max_level; log_level_range_.max_level = max_level;
} }
log_level log_domain::max_log_level() const log_level log_domain::max_log_level() const {
{
return log_level_range_.max_level; return log_level_range_.max_level;
} }
void log_domain::min_log_level(log_level min_level) void log_domain::min_log_level(const log_level min_level) {
{
log_level_range_.min_level = min_level; log_level_range_.min_level = min_level;
} }
log_level log_domain::min_log_level() const log_level log_domain::min_log_level() const {
{
return log_level_range_.min_level; return log_level_range_.min_level;
} }
void log_domain::add_sink(sink_ptr sink) void log_domain::add_sink(sink_ptr sink) {
{ std::lock_guard l(mutex_);
sinks.push_back(std::move(sink)); sinks.push_back(std::move(sink));
} }
void log_domain::log(log_level lvl, const std::string &source, const char *message) const { void log_domain::log(const log_level lvl, const std::string &source, const char *message) const {
if (lvl < log_level_range_.max_level || lvl > log_level_range_.min_level) { if (lvl < log_level_range_.min_level || lvl > log_level_range_.max_level) {
return; return;
} }
@ -97,9 +95,9 @@ void log_domain::log(log_level lvl, const std::string &source, const char *messa
char buffer[1024]; char buffer[1024];
#ifdef _MSC_VER #ifdef _MSC_VER
int ret = sprintf_s(buffer, 1024, "%s [Thread %zu] [%-7s] [%s]: %s\n", timestamp, details::acquire_thread_index(std::this_thread::get_id()), level_strings[lvl].c_str(), source.c_str(), message); const int ret = sprintf_s(buffer, 1024, "%s [Thread %zu] [%-7s] [%s]: %s\n", timestamp, details::acquire_thread_index(std::this_thread::get_id()), level_strings[lvl].c_str(), source.c_str(), message);
#else #else
int ret = sprintf(buffer, "%s [Thread %lu] [%-7s] [%s]: %s\n", timestamp, details::acquire_thread_index(std::this_thread::get_id()), level_strings[lvl].c_str(), source.c_str(), message); const int ret = sprintf(buffer, "%s [Thread %lu] [%-7s] [%s]: %s\n", timestamp, details::acquire_thread_index(std::this_thread::get_id()), level_strings[lvl].c_str(), source.c_str(), message);
#endif #endif
std::lock_guard<std::mutex> l(mutex_); std::lock_guard<std::mutex> l(mutex_);
@ -108,13 +106,12 @@ void log_domain::log(log_level lvl, const std::string &source, const char *messa
} }
} }
void log_domain::clear() void log_domain::clear() {
{ std::lock_guard l(mutex_);
sinks.clear(); sinks.clear();
} }
void log_domain::get_time_stamp(char* const timestamp_buffer) const { void log_domain::get_time_stamp(char* const timestamp_buffer) {
std::lock_guard l(mutex_);
details::gettimestamp(timestamp_buffer, 80); details::gettimestamp(timestamp_buffer, 80);
} }

View File

@ -7,25 +7,25 @@ namespace matador::logger {
std::ostream& operator<<(std::ostream &os, const log_level lvl) std::ostream& operator<<(std::ostream &os, const log_level lvl)
{ {
switch (lvl) { switch (lvl) {
case log_level::LVL_ERROR: case log_level::Error:
os << "ERROR"; os << "ERROR";
break; break;
case log_level::LVL_FATAL: case log_level::Fatal:
os << "FATAL"; os << "FATAL";
break; break;
case log_level::LVL_DEBUG: case log_level::Debug:
os << "DEBUG"; os << "DEBUG";
break; break;
case log_level::LVL_INFO: case log_level::Info:
os << "INFO"; os << "INFO";
break; break;
case log_level::LVL_TRACE: case log_level::Trace:
os << "TRACE"; os << "TRACE";
break; break;
case log_level::LVL_WARN: case log_level::Warn:
os << "WARN"; os << "WARN";
break; break;
case log_level::LVL_ALL: case log_level::All:
os << "ALL"; os << "ALL";
break; break;
default: default:

View File

@ -5,19 +5,18 @@ namespace matador::logger {
log_level_range log_manager::default_log_level_range_ = {}; log_level_range log_manager::default_log_level_range_ = {};
logger log_manager::create_logger(std::string source) const { logger log_manager::create_logger(std::string source) const {
return logger(std::move(source), default_log_domain_); return {std::move(source), default_log_domain_};
} }
logger log_manager::create_logger(std::string source, const std::string &domain_name) { logger log_manager::create_logger(std::string source, const std::string &domain_name) {
return logger(std::move(source), acquire_domain(domain_name)); return {std::move(source), acquire_domain(domain_name)};
} }
void log_manager::add_sink(sink_ptr sink) const { void log_manager::add_sink(sink_ptr sink) const {
default_log_domain_->add_sink(std::move(sink)); default_log_domain_->add_sink(std::move(sink));
} }
void log_manager::add_sink(sink_ptr sink, const std::string &domain_name) void log_manager::add_sink(sink_ptr sink, const std::string &domain_name) {
{
const auto log_domain = acquire_domain(domain_name); const auto log_domain = acquire_domain(domain_name);
log_domain->add_sink(std::move(sink)); log_domain->add_sink(std::move(sink));
@ -27,44 +26,40 @@ void log_manager::clear_all_sinks() const {
default_log_domain_->clear(); default_log_domain_->clear();
} }
void log_manager::clear_all_sinks(const std::string &domain_name) void log_manager::clear_all_sinks(const std::string &domain_name) {
{ std::lock_guard lock(mutex_);
if (const auto it = log_domain_map_.find(domain_name); it != log_domain_map_.end()) { if (const auto it = log_domain_map_.find(domain_name); it != log_domain_map_.end()) {
it->second->clear(); it->second->clear();
} }
} }
void log_manager::clear() void log_manager::clear() {
{ std::lock_guard lock(mutex_);
log_domain_map_.clear(); log_domain_map_.clear();
default_log_domain_->clear();
} }
void log_manager::max_default_log_level(log_level max_level) void log_manager::max_default_log_level(const log_level max_level) {
{
default_log_level_range_.max_level = max_level; default_log_level_range_.max_level = max_level;
} }
log_level log_manager::max_default_log_level() log_level log_manager::max_default_log_level() {
{
return default_log_level_range_.max_level; return default_log_level_range_.max_level;
} }
void log_manager::min_default_log_level(log_level min_level) void log_manager::min_default_log_level(const log_level min_level) {
{
default_log_level_range_.min_level = min_level; default_log_level_range_.min_level = min_level;
} }
log_level log_manager::min_default_log_level() log_level log_manager::min_default_log_level() {
{
return default_log_level_range_.min_level; return default_log_level_range_.min_level;
} }
std::shared_ptr<log_domain> log_manager::acquire_domain(const std::string &name) std::shared_ptr<log_domain> log_manager::acquire_domain(const std::string &name) {
{
if (name == "default") { if (name == "default") {
return default_log_domain_; return default_log_domain_;
} }
std::lock_guard lock(mutex_);
auto it = log_domain_map_.find(name); auto it = log_domain_map_.find(name);
if (it == log_domain_map_.end()) { if (it == log_domain_map_.end()) {
it = log_domain_map_.insert(std::make_pair(name, std::make_shared<log_domain>(name, default_log_level_range_))).first; it = log_domain_map_.insert(std::make_pair(name, std::make_shared<log_domain>(name, default_log_level_range_))).first;
@ -72,34 +67,34 @@ std::shared_ptr<log_domain> log_manager::acquire_domain(const std::string &name)
return it->second; return it->second;
} }
std::shared_ptr<log_domain> log_manager::find_domain(const std::string &name) std::shared_ptr<log_domain> log_manager::find_domain(const std::string &name) {
{
if (name == "default") { if (name == "default") {
return default_log_domain_; return default_log_domain_;
} }
auto it = log_domain_map_.find(name); std::lock_guard lock(mutex_);
if (it != log_domain_map_.end()) { if (const auto it = log_domain_map_.find(name); it != log_domain_map_.end()) {
return it->second; return it->second;
} }
return std::shared_ptr<log_domain>(); return {};
} }
void log_manager::log_default(const log_level lvl, const std::string &source, const char *message) const { void log_manager::log_default(const log_level lvl, const std::string &source, const char *message) const {
default_log_domain_->log(lvl, source, message); default_log_domain_->log(lvl, source, message);
} }
std::shared_ptr<file_sink> create_file_sink(const std::string &logfile) log_manager::log_manager() {
{ default_log_domain_ = std::make_shared<log_domain>("default", default_log_level_range_);
}
std::shared_ptr<file_sink> create_file_sink(const std::string &logfile) {
return std::make_shared<file_sink>(logfile); return std::make_shared<file_sink>(logfile);
} }
std::shared_ptr<stderr_sink> create_stderr_sink() std::shared_ptr<stderr_sink> create_stderr_sink() {
{
return std::make_shared<stderr_sink>(); return std::make_shared<stderr_sink>();
} }
std::shared_ptr<stdout_sink> create_stdout_sink() std::shared_ptr<stdout_sink> create_stdout_sink() {
{
return std::make_shared<stdout_sink>(); return std::make_shared<stdout_sink>();
} }
@ -128,7 +123,7 @@ void domain_min_log_level(const std::string &name, const log_level min_lvl)
void domain_max_log_level(const std::string &name, const log_level max_lvl) void domain_max_log_level(const std::string &name, const log_level max_lvl)
{ {
if (const auto domain = log_manager::instance().find_domain(name)) { if (const auto domain = log_manager::instance().find_domain(name)) {
domain->min_log_level(max_lvl); domain->max_log_level(max_lvl);
} }
} }

View File

@ -18,8 +18,7 @@ using namespace matador::logger;
namespace filehelper { namespace filehelper {
class std_stream_switcher class std_stream_switcher {
{
public: public:
explicit std_stream_switcher(FILE *str, const char* redirect) explicit std_stream_switcher(FILE *str, const char* redirect)
: stream(str) { : stream(str) {
@ -56,8 +55,6 @@ TEST_CASE("Test log file sink", "[logger][log][file_sink]") {
REQUIRE(matador::os::exists("test.txt")); REQUIRE(matador::os::exists("test.txt"));
// UNIT_ASSERT_EQUAL("test.txt", test.path());
test.close(); test.close();
if (::remove("test.txt") == -1) { if (::remove("test.txt") == -1) {
@ -136,28 +133,28 @@ TEST_CASE("Test log rotating file sink", "[logger][log][rotate_file_sink]") {
} }
TEST_CASE("Test log level range", "[logger][level][range]") { TEST_CASE("Test log level range", "[logger][level][range]") {
REQUIRE(log_level::LVL_INFO == log_manager::min_default_log_level()); REQUIRE(log_level::Info == log_manager::min_default_log_level());
REQUIRE(log_level::LVL_FATAL == log_manager::max_default_log_level()); REQUIRE(log_level::Fatal == log_manager::max_default_log_level());
default_min_log_level(log_level::LVL_DEBUG); default_min_log_level(log_level::Debug);
default_max_log_level(log_level::LVL_ERROR); default_max_log_level(log_level::Error);
REQUIRE(log_level::LVL_DEBUG == log_manager::min_default_log_level()); REQUIRE(log_level::Debug == log_manager::min_default_log_level());
REQUIRE(log_level::LVL_ERROR == log_manager::max_default_log_level()); REQUIRE(log_level::Error == log_manager::max_default_log_level());
log_level_range llr; log_level_range llr;
llr.min_level = log_level::LVL_DEBUG; llr.min_level = log_level::Debug;
llr.max_level = log_level::LVL_TRACE; llr.max_level = log_level::Trace;
log_domain ld("test", llr); log_domain ld("test", llr);
REQUIRE(log_level::LVL_DEBUG == ld.min_log_level()); REQUIRE(log_level::Debug == ld.min_log_level());
REQUIRE(log_level::LVL_TRACE == ld.max_log_level()); REQUIRE(log_level::Trace == ld.max_log_level());
ld.min_log_level(log_level::LVL_INFO); ld.min_log_level(log_level::Info);
ld.max_log_level(log_level::LVL_ERROR); ld.max_log_level(log_level::Error);
REQUIRE(log_level::LVL_INFO == ld.min_log_level()); REQUIRE(log_level::Info == ld.min_log_level());
REQUIRE(log_level::LVL_ERROR == ld.max_log_level()); REQUIRE(log_level::Error == ld.max_log_level());
} }
TEST_CASE("Test basic logger functions", "[logger][basic]") { TEST_CASE("Test basic logger functions", "[logger][basic]") {
@ -205,8 +202,8 @@ TEST_CASE("Test basic logger functions", "[logger][basic]") {
} }
TEST_CASE("Test logging", "[logger][logging]") { TEST_CASE("Test logging", "[logger][logging]") {
domain_min_log_level("default", log_level::LVL_FATAL); domain_min_log_level("default", log_level::Fatal);
domain_max_log_level("default", log_level::LVL_TRACE); domain_max_log_level("default", log_level::Trace);
auto logger = create_logger("test"); auto logger = create_logger("test");
@ -230,7 +227,7 @@ TEST_CASE("Test logging", "[logger][logging]") {
logger.trace("tracing something %s", "important"); logger.trace("tracing something %s", "important");
logger.error("big error"); logger.error("big error");
logger.error("big error %s", "important"); logger.error("big error %s", "important");
log(log_level::LVL_ERROR, "test", "global log test %d", 4711); log(log_level::Error, "test", "global log test %d", 4711);
logsink->close(); logsink->close();
@ -308,43 +305,43 @@ TEST_CASE("Test log stderr", "[logger][logging][stderr]") {
TEST_CASE("Test log levels", "[logger][levels]") { TEST_CASE("Test log levels", "[logger][levels]") {
std::stringstream out; std::stringstream out;
out << log_level::LVL_ERROR; out << log_level::Error;
REQUIRE("ERROR" == out.str()); REQUIRE("ERROR" == out.str());
out.str(""); out.str("");
out.clear(); out.clear();
out << log_level::LVL_DEBUG; out << log_level::Debug;
REQUIRE("DEBUG" == out.str()); REQUIRE("DEBUG" == out.str());
out.str(""); out.str("");
out.clear(); out.clear();
out << log_level::LVL_INFO; out << log_level::Info;
REQUIRE("INFO" == out.str()); REQUIRE("INFO" == out.str());
out.str(""); out.str("");
out.clear(); out.clear();
out << log_level::LVL_FATAL; out << log_level::Fatal;
REQUIRE("FATAL" == out.str()); REQUIRE("FATAL" == out.str());
out.str(""); out.str("");
out.clear(); out.clear();
out << log_level::LVL_TRACE; out << log_level::Trace;
REQUIRE("TRACE" == out.str()); REQUIRE("TRACE" == out.str());
out.str(""); out.str("");
out.clear(); out.clear();
out << log_level::LVL_WARN; out << log_level::Warn;
REQUIRE("WARN" == out.str()); REQUIRE("WARN" == out.str());
out.str(""); out.str("");
out.clear(); out.clear();
out << log_level::LVL_ALL; out << log_level::All;
REQUIRE("ALL" == out.str()); REQUIRE("ALL" == out.str());
} }

View File

@ -5,6 +5,7 @@
#include "matador/utils/foreign_attributes.hpp" #include "matador/utils/foreign_attributes.hpp"
#include "matador/object/object_ptr.hpp" #include "matador/object/object_ptr.hpp"
#include "matador/object/collection.hpp"
#include "matador/object/many_to_many_relation.hpp" #include "matador/object/many_to_many_relation.hpp"
#include <utility> #include <utility>
@ -16,7 +17,7 @@ struct course;
struct student { struct student {
unsigned int id{}; unsigned int id{};
std::string name; std::string name;
std::vector<object::object_ptr<course> > courses; object::collection<object::object_ptr<course> > courses;
student() = default; student() = default;
@ -37,7 +38,7 @@ struct student {
struct course { struct course {
unsigned int id{}; unsigned int id{};
std::string title; std::string title;
std::vector<object::object_ptr<student> > students; object::collection<object::object_ptr<student> > students;
course() = default; course() = default;