dab-adapter-cpp学习:DAB::dabclient

概述

dab-adapter-cpp 是一个 C++ 编写的设备自动化总线(DAB)适配器模板,旨在简化 DAB 操作的实现,库处理消息层接口、解析和调度工作,方便开发者专注于所需 DAB 操作的实现。

DAB::dabclient

DAB::dabClient 是实现 DAB 接口所需基本功能的基类,使用 CRTP 模式继承,用于检测功能重写并自动填充 opList DAB 方法,具体实现类需从其继承。

def宏

def是 DAB::dabClient的一个重要的宏,主要用于定义和初始化 DAB(Device Automation Bus)操作相关的信息,并将其插入到 dispatchMap 中。

  • def宏的参数
#define METHODS \
  def( "/operations/list", opList, opList, {}, {} )                                                                                       \
  def( "/applications/list", appList, appList, {}, {} )                                                                               \
  def( "/applications/launch", appLaunch, appLaunch, {"appId"}, {"parameters"} )                                                  \
  def( "/applications/launch-with-content", appLaunchWithContent, appLaunchWithContent, ({ "appId", "contentId" }), { "parameters" } ) 
  • 参数及作用

    • methName
      类型:字符串字面量
      作用:表示 DAB 操作的方法名,例如 "/operations/list"、"/applications/list" 等。这个方法名会被用于构建最终的操作主题(如 "dab/" + deviceId + methName),用于在 dispatchMap 中作为键,以便后续的消息路由和处理。
    • detectFunc
      类型:函数名
      作用:用于检测某个功能是否被重写。在创建 p1 时,会通过 std::is_same_v<decltype(&dabClient::detectFunc), decltype(&T::detectFunc)> 来判断该函数是否被重写,这对于确定某个 DAB 操作是否被用户实现的类重写很有用,从而影响是否将该操作添加到 dispatchMap 中。
    • callFunc
      类型:函数名
      作用:表示实际要调用的函数。nativeDispatch 会存储这个函数指针,当对应的 DAB 操作被触发时,会调用这个函数来处理具体的业务逻辑。
    • fixedParams
      类型:初始化列表
      作用:包含固定参数的列表。这些参数在调用 callFunc 时可能会被使用,例如在某些 DAB 操作中,可能需要特定的固定参数才能正确执行。fixedParams 的大小会用于确定 nativeDispatch 模板实例化时的固定参数数量。
    • optionalParams
      类型:初始化列表
      作用:包含可选参数的列表。与 fixedParams 类似,但这些参数是可选的。optionalParams 的大小会用于确定 nativeDispatch 模板实例化时的可选参数数量。
  • def位置
    在解读这段代码的时候,首先关注了def的位置
    def的使用在def的定义后面,

    • 宏是在 C++ 编译的预处理阶段进行处理的。
      预处理阶段是编译过程的第一个阶段,在这个阶段,预处理器会对源代码进行文本替换操作,而不是像编译和链接阶段那样进行语法分析、语义检查以及生成可执行代码等操作。
    • 宏定义:
      当预处理器遇到 #define 指令(如 #define def(...) ... )时,它会将宏名(这里是 def)及其参数列表和对应的替换文本记录下来。
    • 宏展开:
      在后续的代码中,当预处理器遇到宏名(如 def(...))时,它会将宏调用处的代码用宏定义中的替换文本进行替换。这个过程是简单的文本替换,不涉及语法和语义的检查。
    • 在这段代码中,def 宏的定义和使用也是遵循同样的原则。
      预处理器在处理代码时,会先记录 def 宏的定义,然后在后续遇到 def 宏的使用时,将其替换为相应的代码片段。由于预处理阶段只是简单的文本替换,不关心宏定义和使用的顺序,所以def 的使用可以放在 def 的定义后面。

def宏的展开

在类的开始,定了了METHODS宏,METHODS宏使用def宏定义和初始化 DAB(Device Automation Bus)操作相关的信息。
def宏的在dabClient的构造函数中定义,METHODS宏也在此处展开
展开后的代码类似如下:

 // 展开def( "/applications/launch-with-content", appLaunchWithContent, appLaunchWithContent, ({ "appId", "contentId" }), { "parameters" } ) 
{
    auto disp = std::make_unique<nativeDispatch<std::initializer_list<char const *>{ "appId", "contentId" }.size(), std::initializer_list<char const *>{ "parameters" }.size(), T, decltype(&T::appLaunchWithContent)>> ( &T::appLaunchWithContent, std::vector<std::string_view>{ "appId", "contentId" }, std::vector<std::string_view>{ "parameters" } );
    auto p1 = std::make_pair ( std::move ( disp ), !std::is_same_v<decltype(&dabClient::appLaunchWithContent), decltype(&T::appLaunchWithContent)> || !strcmp ( "/operations/list", ("/applications/launch-with-content") ) || !strcmp ( "/version", ("/applications/launch-with-content") ) );
    auto p2 = std::make_pair ( std::string ( "dab/" ) + deviceId + ("/applications/launch-with-content"), std::move ( p1 ) );
    dispatchMap.insert ( std::move ( p2) );
}

  • disp 的创建:
    • std::make_unique<nativeDispatch<...>> 用于创建一个 nativeDispatch 类型的智能指针 disp。
    • std::initializer_list<char const *>{ "appId", "contentId" }.size() 计算固定参数列表 { "appId", "contentId" } 的大小,std::initializer_list<char const *>{ "parameters" }.size() 计算可选参数列表 { "parameters" } 的大小。
    • &T::appLaunchWithContent 是要调用的函数指针,同时将固定参数列表 { "appId", "contentId" } 和可选参数列表 { "parameters" } 转换为 std::vectorstd::string_view 传递给 nativeDispatch 的构造函数。
  • p1 的创建:
    • std::make_pair 创建一个 pair 对象 p1,第一个元素是 std::move(disp),即移动 disp 的所有权。
    • 第二个元素是一个布尔值,用于判断 appLaunchWithContent 函数是否被重写,同时还会检查方法名是否为 "/operations/list" 或 "/version"。
  • p2 的创建:
    • 再次使用 std::make_pair 创建一个 pair 对象 p2,第一个元素是 "dab/" + deviceId + "/applications/launch-with-content",即构建完整的操作主题。
    • 第二个元素是 std::move(p1),即移动 p1 的所有权。
  • 插入到 dispatchMap 中:
    dispatchMap.insert(std::move(p2)) 将 p2 移动插入到 dispatchMap 中,用于后续的消息路由和处理。

nativeDispatch类

template< size_t nFixed, size_t nOptional, typename T, class R, class C, class ... Args >
struct nativeDispatch<nFixed, nOptional, T, R ( C::* ) ( Args... )> : public dispatcher<T>
{
        nativeDispatch ()
        {
            // should never be called
            assert ( false );
        }

// ...
};

类的模板参数

nativeDispatch类是dispatcher类的派生类,其目的是调用 C++ 方法,并从传入的 JSON 参数中提取参数来调用该方法,同时处理固定参数和可选参数。
nativeDispatch类的模板参数说明如下:
nFixed:表示固定参数的数量,这些参数的值在 JSON 中必须存在。
nOptional:表示可选参数的数量,这些参数的值在 JSON 中可以不存在,若不存在则使用默认构造的值。
T:用于调度的类的类型。
R ( C::* ) ( Args... ):要调用的方法的函数指针,其中R是返回类型,C是类类型,Args...是参数包,表示该方法的参数类型列表。

构造函数参数

    // the constructor takes the function pointer of the method to call, and a vector of fixed and a vector of optional parameters
    nativeDispatch ( R ( C::*func ) ( Args... ), std::vector<std::string_view> const &fixedParams, std::vector<std::string_view> const &optionalParams ) : fixedParams ( fixedParams ), optionalParams ( optionalParams )
    {
        funcPtr = func;
    }

用于初始化nativeDispatch对象,它接收特定的参数来配置该对象,以便能够正确地调用指定的成员函数并处理相应的参数。以下是该构造函数的详细介绍:

  • R ( C::*func ) ( Args... )
    这是一个指向类C的成员函数的指针,该成员函数的返回类型为R,参数列表为Args...。这个参数的作用是指定在进行调度时要调用的具体成员函数。
    例如,假设有一个类MyClass,其中有一个成员函数void myMethod(int a, std::string b),那么R就是void,C就是MyClass,Args...就是int, std::string。
  • std::vector<std::string_view> const &fixedParams
    这是一个常量引用,指向一个存储std::string_view类型元素的向量。fixedParams向量包含了固定参数的名称列表。
    固定参数是指在调用成员函数时必须提供的参数,这些参数在 JSON 数据中必须存在对应的键值对。
    例如,如果成员函数myMethod的第一个参数a是固定参数,那么在fixedParams向量中可能会有一个元素对应着"a",表示在解析 JSON 数据时,需要查找键为"a"的值作为该参数的值。
  • std::vector<std::string_view> const &optionalParams
    这也是一个常量引用,指向一个存储std::string_view类型元素的向量。optionalParams向量包含了可选参数的名称列表。
    可选参数是指在调用成员函数时可以不提供的参数,如果 JSON 数据中没有对应的键值对,则可以使用默认值。
    例如,如果成员函数myMethod的第二个参数b是可选参数,那么在optionalParams向量中可能会有一个元素对应着"b",表示在解析 JSON 数据时,会查找键为"b"的值作为该参数的值,如果没有找到,则使用默认值。

最后两个模板参数的推导

编译器会根据appLaunchWithContent函数的参数来推导nativeDispatch类的最后两个模板参数。
在代码中,nativeDispatch类有一个特化版本:

template< size_t nFixed, size_t nOptional, typename T, class R, class C, class ... Args >
struct nativeDispatch<nFixed, nOptional, T, R ( C::* ) ( Args... )> : public dispatcher<T>
{
    // ...
};

当使用nativeDispatch来处理像appLaunchWithContent这样的成员函数时,编译器会根据appLaunchWithContent的函数签名来推导模板参数。
例如,appLaunchWithContent函数的定义如下:

jsonElement appLaunchWithContent ( std::string const &appId, std::string const &contentId, jsonElement const &elem )
{
    throw dabException{501, "unsupported"};
}

这里,R会被推导为jsonElement(函数的返回类型),C会被推导为包含appLaunchWithContent的类类型(如果appLaunchWithContent是某个类的成员函数),Args...会被推导为std::string const &, std::string const &, jsonElement const &(函数的参数类型列表)。
这样,编译器就可以根据具体的函数签名来实例化nativeDispatch类的特化版本,从而实现对不同函数的适配和调用。
总结来说,编译器能够根据具体的成员函数参数和返回类型,自动推导出nativeDispatch类模板所需的参数,以正确实例化该类。

特化发生的时机

nativeDispatch类的特化是在编译期,当编译器遇到需要实例化nativeDispatch类的代码时,根据具体的模板参数来选择合适的特化版本。具体来说,当创建nativeDispatch对象并传递具体的函数指针(如指向appLaunchWithContent的函数指针)时,编译器会根据这个函数的签名(返回类型、所属类类型以及参数类型)来确定特化版本。
例如,在代码中可能会有类似这样的创建nativeDispatch对象的代码:

// 假设T是某个类类型
R (C::*funcPtr)(Args...) = &appLaunchWithContent;
std::vector<std::string_view> fixedParams = {"appId", "contentId"};
std::vector<std::string_view> optionalParams = {"parameters"};
nativeDispatch<nFixed, nOptional, T, funcPtr>(funcPtr, fixedParams, optionalParams);

在这个过程中,编译器会根据funcPtr所指向的appLaunchWithContent函数的具体信息(返回类型、所属类类型以及参数类型)来实例化nativeDispatch类的特化版本。

std::string_view

std::string_view C++17 引入的一个类,它提供了一种轻量级的方式来处理字符串,主要有以下作用:

  • 避免不必要的字符串拷贝
    在许多情况下,当我们只需要读取字符串的内容而不需要拥有它的所有权时,使用 std::string 会导致不必要的内存拷贝。std::string_view 只包含一个指向字符串数据的指针和字符串的长度,不会进行数据的拷贝,因此可以显著提高性能。
    例如:
#include <iostream>
#include <string_view>

void printStringView(std::string_view sv) {
    std::cout << sv << std::endl;
}

int main() {
    std::string str = "Hello, World!";
    printStringView(str);  // 不会进行字符串拷贝
    printStringView("Hello, C++!");  // 直接处理字符串字面量,无拷贝
    return 0;
}
  • 作为函数参数
    当函数只需要读取字符串内容时,使用 std::string_view 作为参数类型可以接受多种字符串类型,如 std::stringconst char* 等,增加了函数的灵活性。
#include <iostream>
#include <string_view>

void printLength(std::string_view sv) {
    std::cout << "Length: " << sv.length() << std::endl;
}

int main() {
    std::string str = "Hello";
    const char* cstr = "World";
    printLength(str);  // 可以接受 std::string
    printLength(cstr);  // 可以接受 const char*
    return 0;
}
  • 处理子字符串
    std::string_view 可以方便地表示字符串的一部分,而不需要额外的内存分配。
#include <iostream>
#include <string_view>

int main() {
    std::string str = "Hello, World!";
    std::string_view sv = str;
    std::string_view sub_sv = sv.substr(7, 5);  // 表示 "World"
    std::cout << sub_sv << std::endl;
    return 0;
}
  • 提高代码的效率
    在一些需要频繁处理字符串的场景中,使用 std::string_view 可以减少内存分配和拷贝的开销,从而提高代码的效率。
#define def( methName, detectFunc, callFunc, fixedParams, optionalParams ) \
{ \
    auto disp = std::make_unique<nativeDispatch<std::initializer_list<char const *>fixedParams.size (), std::initializer_list<char const *>optionalParams.size (), T, decltype(&T::callFunc)>> ( &T::callFunc, std::vector<std::string_view> fixedParams, std::vector<std::string_view> optionalParams ); \
    auto p1 = std::make_pair ( std::move ( disp ), !std::is_same_v<decltype(&dabClient::detectFunc), decltype(&T::detectFunc)> || !strcmp ( "/operations/list", (methName) ) || !strcmp ( "/version", (methName) ) ); \
    auto p2 = std::make_pair ( std::string ( "dab/" ) + deviceId + (methName), std::move ( p1 ) ); \
    dispatchMap.insert ( std::move ( p2) ); \
}

这里使用 std::vector<std::string_view> 来传递固定参数和可选参数列表,避免了不必要的字符串拷贝,提高了代码的性能。
总之,std::string_view 是一种高效、灵活的字符串处理工具,适用于各种只读字符串操作的场景。

std::initializer_list

std::initializer_listC++11 引入的一个轻量级容器,它提供了一种方便的方式来传递和初始化对象列表。
以下是关于 std::initializer_list 的详细介绍:

  • 基本概念
    • 定义:std::initializer_list 是一个标准库类型,用于表示某种特定类型的值的数组。它可以用于构造函数、函数参数等场景,使得代码在处理多个同类型的值时更加简洁。
    • 特点:
      std::initializer_list 对象是常量,一旦初始化后,其内容就不能被修改。
      它是一个轻量级的容器,不拥有元素的所有权,只是引用传递给它的元素。
  • 语法
std::initializer_list<T>

其中 T 是列表中元素的类型。

  • 常见用法
    • 构造函数初始化
      可以使用 std::initializer_list 来实现构造函数的列表初始化。例如:
#include <vector>

class MyClass {
public:
    MyClass(std::initializer_list<int> list) {
        for (int num : list) {
            std::cout << num << " ";
        }
        std::cout << std::endl;
    }
};

int main() {
    MyClass obj = {1, 2, 3, 4, 5};  // 使用 initializer_list 初始化对象
    return 0;
}

在这个例子中,MyClass 的构造函数接受一个 std::initializer_list<int> 类型的参数,使得我们可以用花括号 {} 来初始化 MyClass 的对象。

  • 函数参数
    函数可以接受 std::initializer_list 作为参数,这样在调用函数时可以方便地传递多个同类型的值。例如:
#include <iostream>

void printNumbers(std::initializer_list<int> numbers) {
    for (int num : numbers) {
        std::cout << num << " ";
    }
    std::cout << std::endl;
}

int main() {
    printNumbers({10, 20, 30, 40, 50});  // 传递 initializer_list 作为参数
    return 0;
}
  • 容器初始化
    许多标准库容器都支持使用 std::initializer_list 进行初始化。例如:
#include <iostream>
#include <vector>

int main() {
    std::vector<int> vec = {1, 2, 3, 4, 5};  // 使用 initializer_list 初始化 vector
    for (int num : vec) {
        std::cout << num << " ";
    }
    std::cout << std::endl;
    return 0;
}
  • 成员函数
    • size():返回列表中元素的数量。
    • begin():返回指向列表中第一个元素的迭代器。
    • end():返回指向列表中最后一个元素之后位置的迭代器。
      例如:
#include <iostream>
#include <initializer_list>

int main() {
    std::initializer_list<int> numbers = {1, 2, 3, 4, 5};
    std::cout << "Size: " << numbers.size() << std::endl;
    for (auto it = numbers.begin(); it != numbers.end(); ++it) {
        std::cout << *it << " ";
    }
    std::cout << std::endl;
    return 0;
}

总之,std::initializer_list 为 C++ 提供了一种方便、统一的方式来处理初始化列表,使得代码更加简洁和易读。

std::is_same_v

std::is_same_vC++17 引入的一个类型特征(type trait),用于在编译时检查两个类型是否相同。它是 std::is_same 的便捷版本。

  • 基本概念
    • 定义:std::is_same_v 是一个编译时的类型检查工具,用于判断两个类型是否完全相同。它返回一个布尔值常量,如果两个类型相同则返回 true,否则返回 false。
    • std::is_same 的关系:在 C++11C++14 中,要进行类型相同的检查,通常使用 std::is_same,它是一个类模板,使用时需要通过 ::value 来获取布尔结果。而 C++17 引入了 std::is_same_v 作为 std::is_same<T, U>::value 的别名,简化了代码的编写。
std::is_same_v<T, U>

其中 T 和 U 是要比较的两个类型。

  • 常见用途
    • 模板元编程:在模板元编程中,经常需要根据类型的不同进行不同的处理,std::is_same_v 可以帮助我们在编译时做出决策。例如:
#include <iostream>
#include <type_traits>

template <typename T>
void printTypeInfo() {
    if constexpr (std::is_same_v<T, int>) {
        std::cout << "这是一个 int 类型" << std::endl;
    } else if constexpr (std::is_same_v<T, double>) {
        std::cout << "这是一个 double 类型" << std::endl;
    } else {
        std::cout << "这是其他类型" << std::endl;
    }
}

int main() {
    printTypeInfo<int>();
    printTypeInfo<double>();
    printTypeInfo<char>();
    return 0;
}

在这个例子中,根据不同的类型,printTypeInfo 函数会输出不同的信息。

  • 编译时类型检查:在函数或类模板中,可以使用 std::is_same_v 来确保传入的类型符合要求,从而避免运行时错误。例如:
#include <iostream>
#include <type_traits>

template <typename T>
T add(T a, T b) {
    static_assert(std::is_same_v<T, int> || std::is_same_v<T, double>, "只支持 int 和 double 类型");
    return a + b;
}

int main() {
    int result1 = add(1, 2);
    double result2 = add(1.5, 2.5);
    // 下面这行代码会在编译时出错,因为传入的是 char 类型
    // char result3 = add('a', 'b');
    return 0;
}

总之,std::is_same_v 是一个非常有用的类型特征,它可以帮助我们在编译时进行类型检查和决策,提高代码的安全性和可维护性。

  • 为什么要引入 std::is_same_v
    引入 std::is_same_v 主要是为了提高代码的简洁性和可读性。在 C++17 之前,使用 std::is_same 进行类型比较时,需要显式地访问 ::value 成员,这会使代码显得冗长。而 std::is_same_v 允许直接获取类型比较的结果,减少了代码量,使代码更加简洁易读。
posted @   荣--  阅读(15)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· DeepSeek “源神”启动!「GitHub 热点速览」
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· C# 集成 DeepSeek 模型实现 AI 私有化(本地部署与 API 调用教程)
· DeepSeek R1 简明指南:架构、训练、本地部署及硬件要求
· 2 本地部署DeepSeek模型构建本地知识库+联网搜索详细步骤
点击右上角即可分享
微信分享提示