富结果(Rich return value)类的回顾
本文是在写过的代码中进行回顾,有理解不对的地方,望请指正!
在库(Library)或框架(Framework)设计中,"Rich return value" 是指返回值的丰富性,意味着函数返回的不仅仅是一个简单的值,而是一个包含了额外信息的复合类型。这样的设计可以提供更多的上下文信息,方便调用者理解和处理函数的执行结果。
这里需要使用Qt5::Core、Qt5::Network来实现基恩士KV8000基于NanoSerialOverTcp的读写,设计的思路参考、借鉴了C#的工业通讯框架HSLCommunication。
#pragma once
#include <QObject>
#include <QString>
#include <tuple>
/// @brief
/// 返回结果
/// IsSuccess 具有是成功的判断
/// Message 返回的详细消息
/// ErrorCode 错误代码
/// Content 返回的内容
/// @tparam ...T
/// 可变模板参数(variadic template parameter)
/// ...是一个包扩展(pack expansion)操作符,用于表示一个或多个模板参数
template <typename... T>
class QICResult
{
public:
QICResult() : IsSuccess(false), Message(""), ErrorCode(0) {}
/// @brief 关注Content
/// @param isSuccess
/// @param message
/// @param errorCode
/// @param ...content
QICResult(bool isSuccess, const QString& message, int errorCode, T... content)
: IsSuccess(isSuccess), Message(message), ErrorCode(errorCode), Content(std::make_tuple(content...))
{
}
private:
/// @brief 不关注Content
/// @param message
/// @param errorCode
QICResult(const QString& message, int errorCode)
: IsSuccess(false), Message(message), ErrorCode(errorCode)
{
}
public:
QString ToMessageString() const
{
return QString("IsSuccess: %1, Message: %2, ErrorCode: %3")
.arg(IsSuccess)
.arg(Message)
.arg(ErrorCode);
}
template <typename... U>
void CopyErrorFromOther(const QICResult<U...>& other)
{
this->IsSuccess = other.IsSuccess;
this->Message = other.Message;
this->ErrorCode = other.ErrorCode;
}
static QICResult CreateSuccessResult(T... content)
{
return QICResult(true, "Success", 0, content...);
}
static QICResult CreateFailedResult(const QString& message)
{
auto result = QICResult(message, -1);
return result;
}
/// @brief
/// ...U是函数模板的参数,而T是类模板的参数。在这个函数中,T和U可能是不同的类型
/// 因此,你需要显式地指定你想要构造哪种QICResult类型,这就是为什么你需要用QICResult<T ...>
/// 如果你不指定T,编译器将无法确定你是想使用函数模板参数U还是类模板参数T,这将导致编译错误或不符合预期的行为
/// @tparam ...U
/// @param other
/// @return
template <typename ... U>
static auto CreateFailedResult(const QICResult<U ...>& other)
{
auto result = QICResult(other.Message, other.ErrorCode);
return result;
}
/// @brief 通过索引获取Content中的元素
/// @tparam U Content的元组索引值,从0开始
/// @return
template <std::size_t Index>
auto GetContent() const
{
static_assert(Index<std::tuple_size_v<decltype(Content)>, "Index out of bounds");
return std::get<Index>(Content);
}
/// @brief
/// 对Content指定的索引设置值,检查类型
/// std::is_same_v 用于比较 std::tuple_element_t<Index, std::tuple<T...>>(元组在指定索引位置的类型)与
/// std::decay_t<U>(传入值的类型,去掉顶层的引用和cv限定符)是否相同,如果它们不相同,static_assert 会失败,产生一个编译错误
/// std::decay_t 是用来移除传递过来的类型的顶层 const、volatile 和引用修饰符
/// 这通常是当你想比较两个可能是不完全相同但基础类型相同的类型时有用的。如果你想保持这些修饰符,那么你可以不使用 std::decay_t
/// @tparam U 对应索引类型U的对象
/// @tparam Index 索引
/// @param value 类型为U的对象值
template <std::size_t Index, typename U>
void SetContent(U&& value)
{
// 编译时检查类型匹配
static_assert(std::is_same_v<std::tuple_element_t<Index, std::tuple<T...>>, std::decay_t<U>>,
"Type mismatch: The provided type does not match the tuple element type at the specified index.");
std::get<Index>(Content) = std::forward<U>(value);
}
public:
bool IsSuccess;
QString Message;
int ErrorCode;
std::tuple<T...> Content;
};
分析
当初编写时,没有过多的思考,一边查资料和自己有局限的理解在需要赶进度的情况下完成,有些地方没有过多的思考,这里将自己理解的在这里总结一下。
1. 可变模板参数(variadic template parameter)/模板参数包(template parameter pack)
template <typename... T> class QICResult 允许你在创建对象时传递不同的类型。当你使用类模板 QICResult 时,你可以为 T 提供任意数量和任意类型的模板参数。
例如,你可以这样创建一个 QICResult 对象:QICResult<int, std::string, float>。在这个例子中,T 被实例化为包含 int、std::string 和 float 三种不同类型的模板参数。
这种能够接受不同类型的类模板在实际开发中非常有用,特别是在需要处理不同类型数据的情况下。通过类模板,你可以实现通用的数据结构或算法,以适应不同类型的数据处理需求。
QICResult<int, QString> result = QICResult<int, QString>::CreateSuccessResult(42, "Hello");
qDebug() << result.ToMessageString();
qDebug() << "Content1: " << result.GetContent<0>(); // 输出 42
qDebug() << "Content2: " << result.GetContent<1>(); // 输出 "Hello"
2. 完美转发
template <std::size_t Index, typename U>
void SetContent(U&& value)
{
// 编译时检查类型匹配
static_assert(std::is_same_v<std::tuple_element_t<Index, std::tuple<T...>>, std::decay_t<U>>,
"Type mismatch: The provided type does not match the tuple element type at the specified index.");
std::get<Index>(Content) = std::forward<U>(value);
}
通过上面的代码来做写一些分析,如果修改为std::get<Index>(Content) = value
会是什么样子呢?
使用 std::forward<U>(value)
和直接将 value 传递给 std::get<Index>(Content)
之间的区别在于参数的传递方式。
-
使用
std::forward<U>(value):
std::get<Index>(Content) = std::forward<U>(value);
在这种情况下,value 会根据其类型的右值或左值特性进行完美转发。如果 U 是左值引用类型,那么 value 会被传递为左值引用;如果 U 是右值引用类型,那么 value 会被传递为右值引用。这样做的好处是可以保留传递给 SetContent 方法的参数的引用类型特性,并且可以将右值传递给std::get<Index>(Content)
,从而避免不必要的拷贝。 -
直接传递 value:
std::get<Index>(Content) = value;
在这种情况下,value 会被传递为它原本的引用类型。如果 value 是左值引用,那么std::get<Index>(Content)
将接收到一个左值引用;如果 value 是右值引用,那么std::get<Index>(Content)
将接收到一个右值引用。这种方式不会考虑参数的右值或左值特性,直接按原样传递参数。
3. 引用折叠
在模板函数 SetContent 中,虽然使用了右值引用 U&&,但是 value 参数实际上可以是左值也可以是右值。这是由于引用折叠(reference collapsing)的机制。引用折叠指的是,当一个模板参数的引用类型是一个引用的右值引用(如 T&&)时,该引用的左值特性和右值特性会根据参数的类型进行折叠。
具体来说,在 SetContent 中,如果传递的参数是一个左值,U 会被推导为左值引用类型;如果传递的参数是一个右值,U 会被推导为右值引用类型。这样,无论传递的参数是左值还是右值,都可以通过 std::forward<U>(value)
正确地进行转发。
因此,虽然函数签名是 template <std::size_t Index, typename U> void SetContent(U&& value)
,但传递的参数可以是左值也可以是右值。
引用折叠规则如下:
如果一个类型被折叠成左值引用和右值引用,结果是一个左值引用。
如果两个右值引用被折叠,结果是一个右值引用。
如果两个左值引用被折叠,结果是一个左值引用。
这个规则简单来说就是,当引用类型发生折叠时,会根据引用的特性进行合并。在模板函数中,通过引用折叠规则,可以正确地处理模板参数的右值和左值特性,从而实现完美转发。
4. 引用折叠发生的场景
引用折叠通常确实发生在模板的通用引用传递的情况下,但它也可能发生在其他情况下。除了模板函数的通用引用传递外,引用折叠还可能发生在类型别名(type aliasing)、模板类型推导(template type deduction)以及返回类型推断(return type deduction)等场景中。
模板函数的通用引用传递:当模板函数接受一个通用引用作为参数时,根据传递给它的参数类型(左值或右值),通用引用可能被实例化为左值引用或右值引用,并且可能发生引用折叠。
类型别名(type aliasing):在类型别名中使用通用引用时,也可能导致引用折叠。例如:
template<typename T>
using Ref = T&&;
int main() {
int x = 5;
Ref<int&> ref1 = x; // 引用折叠为 int&
Ref<int&&> ref2 = std::move(x); // 引用折叠为 int&&
return 0;
}
模板类型推导(template type deduction):在模板类型推导时,通用引用可能被推导为左值引用或右值引用,并且可能发生引用折叠。例如:
template<typename T>
void func(T&& arg) {
// 根据 T 的类型进行相应的操作
}
int main() {
int x = 5;
func(x); // 推导为 int&
func(std::move(x)); // 推导为 int&&
return 0;
}
返回类型推断(return type deduction):在返回类型推断时,通用引用可能被推导为左值引用或右值引用,并且可能发生引用折叠。例如:
template<typename T>
auto&& forward(T&& arg) {
return std::forward<T>(arg);
}
int main() {
int x = 5;
auto&& ref1 = forward(x); // 引用折叠为 int&
auto&& ref2 = forward(std::move(x)); // 引用折叠为 int&
return 0;
}
在这些情况下,通用引用的实例化和引用折叠是根据模板参数的具体类型以及传递给模板的参数类型来确定的。因此,引用折叠并不仅限于模板函数的通用引用传递,它可能发生在许多其他情况中。