您可能会问为什么还要使用另一个记录器-答案很简单。我正在开发的该日志记录库并不是专门发布的,它纯粹是出于我自己的经验和乐趣。但是,如果事实证明它至少是不错的,那么为什么呢? br />lwlog.cpp
#pragma once
#include <iostream>
#include <string>
#include <cctype>
#include "core.h"
#include "detail.h"
#include "datetime.h"
#include "registry.h"
namespace lwlog
{
enum class log_level
{
all = (1 << 0),
info = (1 << 1),
warning = (1 << 2),
error = (1 << 3),
critical = (1 << 4),
debug = (1 << 5),
none = (1 << 6)
};
static log_level operator |(log_level lhs, log_level rhs)
{
return static_cast<log_level> (
static_cast<std::underlying_type<log_level>::type>(lhs)
| static_cast<std::underlying_type<log_level>::type>(rhs)
);
}
static log_level operator &(log_level lhs, log_level rhs)
{
return static_cast<log_level> (
static_cast<std::underlying_type<log_level>::type>(lhs)
& static_cast<std::underlying_type<log_level>::type>(rhs)
);
}
class LWLOG logger
{
private:
std::string m_message;
std::string m_loggerName;
std::string m_pattern;
std::string m_logLevel;
log_level m_logLevel_visibility = log_level::all;
std::unordered_map<std::string, std::string> m_patterns_data;
private:
void print_formatted(const std::string& message, std::string pattern);
public:
explicit logger(const std::string& name);
~logger();
void set_name(const std::string& loggerName);
void set_logLevel_visibility(log_level logLevel);
void set_pattern(const std::string& pattern);
void info(const std::string& message);
void warning(const std::string& message);
void error(const std::string& message);
void critical(const std::string& message);
void debug(const std::string& message);
inline std::string get_name() const { return m_loggerName; }
inline std::string get_logLevel() const { return m_logLevel; }
};
template<typename... Args>
void print(std::string format_str, Args&&... args)
{
std::vector<std::string> format_string_tokens_vec;
std::vector<int> format_numeric_tokens_vec;
std::vector<std::string> variadic_arguments_vec;
std::regex reg("(\{\d\})");
(detail::populate_vec_with_variadic_params(variadic_arguments_vec, std::forward<Args>(args)), ...);
detail::populate_vec_with_regex_matches_from_str(format_string_tokens_vec, reg, format_str);
detail::remove_duplicates_in_vec(variadic_arguments_vec);
detail::string_to_numeric_vec(format_string_tokens_vec, format_numeric_tokens_vec, "{}");
for (int i = 0; i < format_string_tokens_vec.size(); ++i)
{
detail::replace_in_string(format_str, format_string_tokens_vec[i], variadic_arguments_vec[format_numeric_tokens_vec[i]]);
}
std::cout << format_str;
}
}
registry.h
#include "lwlog.h"
namespace lwlog
{
logger::logger(const std::string& logger_name)
: m_loggerName(logger_name), m_pattern("[%d, %x] [%l] [%n]: %v")
{
if (registry::instance().is_registry_automatic() == true)
{
registry::instance().register_logger(this);
}
}
logger::~logger()
{
}
void logger::set_name(const std::string& logger_name)
{
m_loggerName = logger_name;
}
void logger::set_logLevel_visibility(log_level logLevel)
{
if (logLevel == log_level::all)
{
m_logLevel_visibility = log_level::info | log_level::warning | log_level::error
| log_level::critical | log_level::debug;
}
else if (logLevel == log_level::none)
{
m_logLevel_visibility = log_level::none;
}
else
{
m_logLevel_visibility = logLevel;
}
}
void logger::set_pattern(const std::string& pattern)
{
m_pattern = pattern;
}
void logger::info(const std::string& message)
{
m_message = message;
m_logLevel = "info";
if (static_cast<std::underlying_type_t<log_level>>(m_logLevel_visibility)
& static_cast<std::underlying_type_t<log_level>>(log_level::info))
{
print_formatted(message, m_pattern);
}
}
void logger::warning(const std::string& message)
{
m_message = message;
m_logLevel = "warning";
if (static_cast<std::underlying_type_t<log_level>>(m_logLevel_visibility)
& static_cast<std::underlying_type_t<log_level>>(log_level::warning))
{
print_formatted(message, m_pattern);
}
}
void logger::error(const std::string& message)
{
m_message = message;
m_logLevel = "error";
if (static_cast<std::underlying_type_t<log_level>>(m_logLevel_visibility)
& static_cast<std::underlying_type_t<log_level>>(log_level::error))
{
print_formatted(message, m_pattern);
}
}
void logger::critical(const std::string& message)
{
m_message = message;
m_logLevel = "critical";
if (static_cast<std::underlying_type_t<log_level>>(m_logLevel_visibility)
& static_cast<std::underlying_type_t<log_level>>(log_level::critical))
{
print_formatted(message, m_pattern);
}
}
void logger::debug(const std::string& message)
{
m_message = message;
m_logLevel = "debug";
if (static_cast<std::underlying_type_t<log_level>>(m_logLevel_visibility)
& static_cast<std::underlying_type_t<log_level>>(log_level::debug))
{
print_formatted(message, m_pattern);
}
}
void logger::print_formatted(const std::string& message, std::string pattern)
{
m_patterns_data.emplace("%d", datetime::get_current_date());
m_patterns_data.emplace("%L", std::string(1, toupper(m_logLevel[0])));
m_patterns_data.emplace("%l", m_logLevel);
m_patterns_data.emplace("%n", m_loggerName);
m_patterns_data.emplace("%v", message);
m_patterns_data.emplace("%x", datetime::get_current_time());
std::vector<std::string> pattern_string_tokens_vec;
std::regex rg("(\%[DdLlnvx]{1})");
detail::populate_vec_with_regex_matches_from_str(pattern_string_tokens_vec, rg, pattern);
for (int i = 0; i < pattern_string_tokens_vec.size(); ++i)
{
for (auto it = m_patterns_data.begin(); it != m_patterns_data.end(); ++it)
{
if (pattern_string_tokens_vec[i] == it->first)
{
detail::replace_in_string(pattern, pattern_string_tokens_vec[i], it->second);
}
}
}
print("{0} \n", pattern);
m_patterns_data.clear();
}
}
registry.cpp
#pragma once
#include <vector>
#include <memory>
#include <string>
#include <unordered_map>
#include "core.h"
namespace lwlog
{
class logger;
class LWLOG registry
{
private:
registry() {}
std::unordered_map<std::string, logger*> m_loggersMap;
bool m_automatic_registry = true;
public:
registry(const registry&) = delete;
registry& operator=(const registry&) = delete;
void register_logger(logger* new_logger);
void drop(const std::string& logger_name);
void drop_all();
void set_automatic_registry(bool automatic);
void display_all_loggers();
inline bool is_registry_automatic() { return m_automatic_registry ? true : false; }
static registry& instance()
{
static registry s_instance;
return s_instance;
}
};
}
detail.h
#include "registry.h"
#include "lwlog.h"
namespace lwlog
{
void registry::register_logger(logger* new_logger)
{
m_loggersMap.emplace(new_logger->get_name(), new_logger);
}
void registry::drop(const std::string& logger_name)
{
for (auto it = m_loggersMap.begin(); it != m_loggersMap.end(); ++it)
{
if (it->first == logger_name)
{
m_loggersMap.erase(logger_name);
break;
}
}
}
void registry::drop_all()
{
m_loggersMap.clear();
}
void registry::set_automatic_registry(bool automatic)
{
m_automatic_registry = automatic;
}
void registry::display_all_loggers()
{
for (auto it = m_loggersMap.begin(); it != m_loggersMap.end(); it++)
{
print("{0} \n", it->first);
}
}
}
datetime.h
#pragma once
#include <regex>
#include <vector>
#include <map>
#include <unordered_set>
namespace detail
{
static void populate_vec_with_regex_matches_from_str(std::vector<std::string>& v, std::regex rg, std::string& s)
{
std::smatch matches;
std::string temp = s;
while (std::regex_search(temp, matches, rg))
{
v.push_back(matches.str(1));
temp = matches.suffix().str();
}
}
template<typename... Args>
void populate_vec_with_variadic_params(std::vector<std::string>& v, Args&&... args)
{
std::vector<std::string> vec =
{
[](auto && arg)
{
if constexpr (std::is_same_v<std::remove_reference_t<decltype(arg)>, char>)
{
return std::string(1, arg);
}
else if constexpr (std::is_arithmetic_v<std::remove_reference_t<decltype(arg)>>)
{
return std::to_string(std::forward<decltype(arg)>(arg));
}
else
{
return arg;
}
}(std::forward<Args>(args))...
};
for (const auto& i : vec)
{
v.push_back(i);
}
}
template<typename T, typename T1>
void string_to_numeric_vec(std::vector<T>& sv, std::vector<T1>& nv, const char* chars_to_remove)
{
for (int i = 0; i < sv.size(); ++i)
{
std::string temp = sv[i];
for (int i = 0; i < strlen(chars_to_remove); ++i)
{
temp.erase(std::remove(temp.begin(), temp.end(), chars_to_remove[i]), temp.end());
}
nv.push_back(std::stoi(temp));
}
}
template<typename T>
void remove_duplicates_in_vec(std::vector<T>& v)
{
std::unordered_set<T> s;
for (const auto& i : v)
{
s.insert(i);
}
v.assign(s.begin(), s.end());
}
static void replace_in_string(std::string& s, const std::string& to_replace, const std::string& replace_with)
{
size_t index = 0;
while (true)
{
index = s.find(to_replace, index);
if (index == std::string::npos)
{
break;
}
s.replace(index, to_replace.length(), replace_with);
index += to_replace.length();
}
}
}
core.h
#pragma once
#include <iomanip>
#include <sstream>
#include <chrono>
#include <ctime>
namespace lwlog
{
namespace datetime
{
static std::string get_current_time_and_date(const char* format)
{
auto now = std::chrono::system_clock::now();
auto in_time_t = std::chrono::system_clock::to_time_t(now);
std::stringstream ss;
ss << std::put_time(std::localtime(&in_time_t), format);
return ss.str();
}
static std::string get_current_time()
{
return get_current_time_and_date("%X");
}
static std::string get_current_date()
{
return get_current_time_and_date("%Y-%m-%d");
}
}
}
这是供记录器使用的沙箱(示例用法)
Sandbox.cpp
#pragma once
#ifdef _WIN32
#define LWLOG_PLATFORM_WINDOWS
#endif
#ifdef __linux__
#define LWLOG_PLATFORM_LINUX
#endif
#ifdef __APPLE__
#define LWLOG_PLATFORM_MAC
#endif
#ifndef LWLOG
#ifdef LWLOG_PLATFORM_WINDOWS
#if defined(LWLOG_BUILD_DLL)
#define LWLOG __declspec(dllexport)
#elif !defined(LWLOG_BUILD_STATIC)
#define LWLOG __declspec(dllimport)
#else
#define LWLOG
#endif
#else
#if __GNUC__ >= 4
#define LWLOG __attribute__((visibility("default")))
#else
#define LWLOG
#endif
#endif
#endif
仍然没有文档,但是我想这很容易导航,因为有一个Sandbox项目供您使用该库的功能。 />
欢迎提供任何有关代码质量和性能的建议,意见以及功能建议。我建议在性能方面仔细研究lwlog.h中的打印方法,也许还有detail.h文件的内容。
P.S.请注意,我是几天前启动的,所以它的功能不是那么丰富,因此再次欢迎您提供有关功能的任何建议。当涉及多线程记录器时,线程安全,异步和登录文件-我想到了这些功能,将尽我所能实现它们。另外,彩色日志是我正在使用ANSI转义码进行的工作。
#1 楼
已经给出了很多好的代码注释。我将专注于非代码方面。请DevOps工程师来解决这个问题,该工程师可以在时间压力下定期对日志中的复杂,陌生的系统进行故障排除。始终保持一致的格式,并且默认情况下具有时间戳和位置(源文件和行),看起来您请求的日期带有格式说明符。您需要上下文(如果有)(例如请求ID)。您还希望轻松地打印非Pod类型,而依靠
to_string()
的设计只能接受Pod,这看起来是不可能的。通常,您会使用基于从std::ostream
继承的内容的设计,以便您的常规operator<<(ostream&, const T&)
可以正常工作。和我使用rdbuf(...)注入到cout
中的std::clog
,然后创建了一组宏来打印位置和时间,例如: #define ERROR std::clog<<date_time_now()<<", status=ERROR ("<<__FILE__<<":"<<__LINE__<<"): "
ERROR << "Honey, I shrunk the kids! " << m_kids << std::endl;
这也是使行号和文件名自动进入日志行的唯一方法。除非我不知道c ++ 17 / 2x中有什么新热点。
请原谅简短和缺乏格式,我在电话上键入此内容。
评论
\ $ \ begingroup \ $
使这种类似于宏的函数放下不是更好;在行尾?
\ $ \ endgroup \ $
–val说恢复莫妮卡
19年6月9日在9:55
\ $ \ begingroup \ $
一切为您服务。分号是意外的习惯力量。
\ $ \ endgroup \ $
–艾米莉·L。
19年6月9日在11:24
\ $ \ begingroup \ $
关于新的热点,source_location已获得设计批准,因此我们很有可能及时看到C ++ 2b的标准化,并最终有了捕获该信息的好方法。
\ $ \ endgroup \ $
–克里斯
19年6月10日在1:56
#2 楼
我看到许多可以帮助您改进代码的事情。将接口与实现分开
接口进入头文件和实现(即所有实际上会发出包括所有功能和数据的字节),应放在单独的
.cpp
文件中。在这种情况下,实际上datetime.h
中的所有内容和static
中的所有detail.h
函数实际上都不应该是static
,而应该分成.h
和.cpp
文件。对于log_level
中的两个lwlog.h
运算符,这都是相同的。确保已具备所有必需的
#include
s 代码在
std::string
中使用datetime.h
,但未使用#include <string>
。此外,根据上述建议,仔细考虑哪些#include
是接口的一部分(并属于.h
文件),以及哪些是实现的一部分,请遵循上述建议。注意带符号和无符号
在当前的
string_to_numeric_vec()
例程中,循环整数i
是有符号的int
值,但将其与返回format_string_tokens_vec.size()
的数量std::size_t
进行比较。最好将i
声明为std::size_t
。修复错误
现在,
remove_duplicates_in_vec
例程错误地反转了传递参数的顺序,因此打印的消息显示“进行消息测试。”这显然不是预期的。另外,如果我们这样称呼它:lwlog::print("That's a {0} message {1} you. \n\n", str1, "{1}");
它显示“这是测试您的测试消息”。这也值得怀疑。
重新考虑方法
在我看来,
regex
,unordered_set
和vector
变量的使用似乎过于复杂。大量浪费的工作也完成了,例如在print_formatted
中设置所有变量,而不仅仅是实际使用的变量。此外,使用可变参数模板而不是可变函数意味着大量的代码膨胀。在使用gcc
和-O2
优化的64位Linux机器上,添加这五行代码将使可执行文件的大小增加11120个字节。 lwlog::print("{0}\n", 1);
lwlog::print("{0}\n", 1.0f);
lwlog::print("{0}\n", 1u);
lwlog::print("{0}\n", 1l);
lwlog::print("{0}\n", "no");
另外,此代码是不是线程安全的。如果我正在写这篇文章,我可能会以类似
std::basic_osyncstream
开头。在可行的地方使用
const
有很多地方,例如
reg
中的print
声明最好像static const
一样。避免在
#include
s中使用相对路径通常最好从
#include
文件中省略相对路径名,而是将编译器指向适当的位置。 /> #include "lwlog/lwlog.h"
对于gcc,您可以使用
-I
。这样可以减少代码对实际文件结构的依赖,并将这些详细信息放在一个位置:Makefile或编译器配置文件。在
return 0
末尾省略main
在C和C ++中,编译器将自动创建与
return 0;
完全等效的代码,因此可以安全地将其省略。评论
\ $ \ begingroup \ $
老兄,我讨厌在主函数末尾省略return语句已成为Code Review的标准功能。这不是一个好的建议。是的,这是合法的,编译器将代您静默生成等效代码,但这并不是一个好主意。好的代码是清晰明确的。因此,好的代码应在main函数的末尾返回EXIT_SUCCESS。
\ $ \ endgroup \ $
–科迪·格雷
19年6月9日在6:25
\ $ \ begingroup \ $
@Edward不行,但是void函数不会像main()对OS那样在其他地方返回int。我们确实写了“ return 0;”。在每个返回int的函数的末尾。
\ $ \ endgroup \ $
–马夫里克
19年6月9日在8:50
\ $ \ begingroup \ $
@Edward main不会返回void,它会变成int。 void是无价值的,int不是。这就是为什么我们省略return void的原因。在void返回函数的末尾(即使允许)。你的问题是一个稻草人。这并没有动机,为什么我们不应该明确说明main的收益。
\ $ \ endgroup \ $
–艾米莉·L。
19年6月9日在8:54
\ $ \ begingroup \ $
@Edward我没有问题,您可以忽略返回值或您重视的代码类型。我担心的是,省略它的过程似乎是最佳做法,而显然并没有这样确定。作为个人建议,这是个人喜好,我认为这是错误的信息。
\ $ \ endgroup \ $
–艾米莉·L。
19年6月9日在11:36
\ $ \ begingroup \ $
我会在以后的答案中考虑一种更好的方式来传达这个想法。感谢您的反馈。
\ $ \ endgroup \ $
–爱德华
19年6月9日在12:08
#3 楼
使头文件中的功能静态意味着每个翻译单元都有自己的单独定义。这通常不是我们想要的。标准做法是在标头中声明该函数,然后在
.cpp
文件中对其进行定义,例如:// header:
#include <string>
namespace lwlog
{
namespace datetime
{
std::string get_current_time_and_date(const char* format);
}
}
。
// .cpp:
#include <iomanip>
#include <sstream>
#include <chrono>
#include <ctime>
namespace lwlog
{
namespace datetime
{
static std::string get_current_time_and_date(const char* format)
{
auto now = std::chrono::system_clock::now();
auto in_time_t = std::chrono::system_clock::to_time_t(now);
std::stringstream ss;
ss << std::put_time(std::localtime(&in_time_t), format);
return ss.str();
}
}
}
虽然这很麻烦,但是这意味着包括
datetime.h
在内的任何人也不会获得sstream
chrono
等标题,这是不必要的副作用。优选声明变量尽可能接近使用点,并(理想情况下)直接分配必要的值,例如用于打印功能中的局部变量。
描述性名称很好,但是像
detail::populate_vec_with_variadic_params
这样的名称可能会过分杀伤。也许改为detail::string_vec_from_args
?同样使用局部变量:variadic_arguments_vec
-> arguments
,format_string_tokens_vec
-> format_tokens
。用于打印的各种
detail
函数可以大大简化:而不是
populate_vec_with_variadic_params
,我们可以定义自己的to_string函数,然后执行以下操作:namespace detail
{
std::string to_string(std::string s) { return s; }
template<class T>
std::string arg_to_string(T&& t)
{
using detail::to_string;
using std::to_string;
return to_string(std::forward<T>(t));
}
} // detail
...
auto arguments = std::vector<std::string>{ detail::arg_to_string(std::forward<Args>(args))... };
这具有允许用户使用的优点。还要为自己的类指定to_string函数。
populate_vec_with_regex_matches_from_str
:应该' t引用
s
,因为它不会更改s
(这使我们可以消除temp变量)。应简单地返回一个向量,而不是更改一个向量。
应使用迭代器以避免不必要的复制(既在内部,又在向量中返回)。
不是捕获大括号然后再将其删除,我们只需捕获大括号内的数字即可。
我们需要捕获括号内的一位或多位数字,n可能只有一个数字字符,否则当我们获取10个参数时,该函数将停止工作。
/>
remove_duplicates_in_vec
:可以致电
std::unique
。 应应用于
format_strings_tokens_vec
而不是variadic_arguments_vec
!我们要删除重复的索引,而不是重复的参数。string_to_numeric_vec
:< br可以使用
std::transform
。应该产生
std::size_t
的索引(又名std::vector<T>::size_type
),而不是int
。应该对转换失败(甚至只是断言)进行某种错误处理。
std::from_chars
可能是为此可用的最佳标准转换器。using substring_view = std::pair<std::string::const_iterator, std::string::const_iterator>;
std::vector<substring_view> find_matches(std::regex rg, std::string const& s)
{
auto result = std::vector<substring_view>();
auto begin = s.cbegin();
auto const end = s.cend();
std::smatch match;
while (std::regex_search(begin, end, match, rg))
{
result.push_back({ match[1].first, match[1].second });
begin = match.suffix().first;
}
return result;
}
...
auto format_strings = detail::find_matches(std::regex("\{(\d+)\}"), format_str);
>
对每个格式令牌索引调用
replace_in_string
,并且每次都对整个字符串进行迭代,从而将复杂度从O(n)转换为O(n ^ 2)。 Yikes!如果我们在进行正则表达式搜索的同时执行“替换”,我们的工作量将大大减少。
所以我可能会执行以下操作:
std::vector<std::size_t> convert_indices(std::vector<substring_view> const& s)
{
auto result = std::vector<std::size_t>(s.size(), 0);
auto convert = [] (substring_view const& v)
{
auto const begin = &*v.first;
auto end = &*v.second;
auto value = std::size_t{ 0 };
auto result = std::from_chars(begin, end, value);
assert(result.ptr == end);
assert(result.ec == std::errc{ });
return value;
};
std::transform(s.begin(), s.end(), result.begin(), convert);
return result;
}
std::cout,而不用构建格式化的字符串。
#4 楼
除了现有的出色答案之外,还有一些其他建议:支持记录堆栈跟踪-这很容易!
Standard C ++不提供获取堆栈跟踪的功能,因此传统上-记录库和手动记录已放弃了这些。但是,这些跟踪对于检查日志和调试程序非常有用(尽管它们很冗长)。还有其他语言的开发人员,尤其是非编译语言的开发人员,因为没有它们而嘲笑我们!
最近,Antony Polukhin(magic_get的声望)承担了组合可用的特定于平台的堆栈的任务将库移到名为stacktrace的单个多平台Boost库中。它的普通香草用法很简单:
#include <boost/stacktrace.hpp>
// ... etc. etc. ...
std::cout << boost::stacktrace::stacktrace();
,它具有用堆栈跟踪等装饰异常的功能。
考虑使用字符串视图代替std :: string引用
如果将临时字符串传递给带有
const string&
的函数,但是随后使用它初始化一些std::string
,则会得到-是的,您知道它-您会得到字符串副本。实际上,看来您只是这样做。 另外,
std::string
仍然存在。您是否真的要进行一堆堆分配?当然不会;或至少-尽可能少。此外,您可能会强迫任何将字符串传递给您的人构造一个std::string
并了解std::string
。我也会尽量避免这种情况。那么,还有什么选择呢?
std::string_view
。它不是完美的(从某种意义上说,使用时您必须小心一点),但这仍然是一个不错的主意:std::string_view
比const std::string&
快多少?如果您正在编写C ++ 17之前的代码,则可以在准则支持库的实现中找到字符串视图。
#5 楼
您已经有了一些好的答案。我的批评来自一个词。 “ printf”。故障查找的问题不仅在于知道每个部分所报告的内容,而且还知道它们以什么顺序发生。任何实用的日志记录库迟早都会碰到线程,这时会出错。
对于初学者来说,我们需要考虑线程安全的打印。从两个线程同时调用时,printf的行为尚未定义,但是通常您会发现一个中断另一个中间打印以打印其自己的文本,然后完成下一个。结果不是很容易理解。
我们还需要考虑“ Heisenbugs”,其中测试代码会更改系统的行为,从而使您的故障查找无效。 printf相对较慢,因此在完成时,第二个线程的交互导致您正在研究的错误将处于完全不同的状态。
最后,您可能希望登录几个不同的方向。也许您希望普通级别的消息进入屏幕,冗长的消息进入主日志文件,而关键消息进入安全注册表。您不能在这里这样做。
我自己建立了一个日志库来解决这些问题。 (这是几年前的,当时可用的库不够好。)有几个关键功能:-
在线程中记录错误的函数存储了日志详细信息。 br />然后该函数将其原子传递给单例日志存储,该存储将日志详细信息推入FIFO。 (实际上是指针的FIFO,因为您不想浪费时间来复制内容。)
以固定的间隔和低优先级,日志存储会弹出一批FIFO条目并将其发送到一个或多个已注册的日志中打印机。
每个日志打印机都有用于设置其感兴趣的日志级别的选项。在单独的线程中运行并再次以低优先级运行,每个日志打印机都将其感兴趣的那些日志的详细信息转储到其选定的目的地。
这是一个非常复杂的结构,输出端需要更大的重量,但是结果是使执行期间的日志记录几乎不明显。这使我们能够使用日志记录来准确地发现应用程序中控制硬件,数据库,用户界面,文件等的线程之间发生的问题。
评论
如果让您感觉更好,我在代码审查方面的经验是,他们不认为您会“重新发明”。它们只是有助于改善您的代码。是的,知道了。我只想对代码的改进进行建设性的批评,意见和建议
为什么m_message是一个字段?除非我错过了它,否则任何人都不会读它。为什么m_logLevel是一个字段,而不是像消息一样传递给print方法?如果曾经在多个线程中使用过此方法,则可能会发生不良情况。我想目的是每个线程都将使用自己的记录器?
@David不在标准C ++中。您必须编写特定于平台的代码才能从异常中获取堆栈跟踪。
@DavidConrad:请参阅我的回答(以及我的最后评论)。