【C++ 17 新功能 std::visit 】深入解析 C++17 中的 std::visit:从原理到实践

简介: 【C++ 17 新功能 std::visit 】深入解析 C++17 中的 std::visit:从原理到实践

1. 引言

1.1 C++17 新特性简介

C++17 是一个标准化的 C++ 语言版本,该版本引入了许多新特性,旨在简化编程任务,提高代码效率,并增强类型安全。从 std::optional(可选值)到 std::filesystem文件系统操作),这些新特性都体现了现代 C++ 的设计理念:让复杂事物变得简单,而不是简单事物变得复杂。

你可能熟悉 Bjarne Stroustrup(C++ 的创造者)的名言:“C++ 旨在让你有能力做到你想做的事,但你必须更加明确地表达你的意图。” 在 C++17 中,你会发现这一点更为明显。

1.2 std::variantstd::visit 的重要性

1.2.1 为什么需要 std::variant(变体)?

在很多编程场景中,我们经常遇到需要处理多种类型的情况。传统上,这可以通过多种方式来实现,例如使用 unionvoid* 指针,甚至使用一系列的 if-else 语句和类型转换。但这些方法通常都有各种缺点,如类型不安全、容易出错或难以维护。

std::variant 为这一问题提供了一个现代、类型安全的解决方案。它允许你在一个单一的变量中存储多种不同的类型,并能在运行时安全地访问它们。

1.2.2 std::visit(访问器)的作用

当你使用 std::variant 时,一个自然而然的问题是如何处理存储在其中的不同类型的值。手动检查和处理多种可能的类型通常很繁琐,而且容易出错。这就是 std::visit 发挥作用的地方。

std::visit 提供了一种机制,让你能够方便、优雅地处理 std::variant 中存储的多种可能的类型。它基于访问者模式(Visitor Pattern),是一种运行时多态的实现。

1.2.3 从人性出发,为什么这两者如此重要

想象一下,你手头有一把瑞士军刀。这把刀有各种功能,但你却不知道如何正确、安全地使用它。这就像拥有 std::variant 但不知道如何有效地使用它。而 std::visit 就像是一个详细的使用手册,它教你如何安全、高效地使用这把瑞士军刀。

1.3 代码示例

让我们先来看一个简单的例子,这将帮助你更好地理解 std::variantstd::visit 的基本用法。

#include <iostream>
#include <variant>
#include <string>
int main() {
    std::variant<int, double, std::string> myVariant = "Hello, world!";
    std::visit([](auto&& arg) {
        std::cout << "The value is: " << arg << std::endl;
    }, myVariant);
    return 0;
}

在这个例子中,myVariant 可以存储 intdoublestd::string 类型的值。我们使用 std::visit 来访问存储在 myVariant 中的值,并输出它。

这里,std::visit 接受了一个 lambda 表达式作为参数,这个 lambda 表达式可以接受任何类型的参数(由 auto&& 指定),然后输出这个参数。

2. 什么是 std::variant

2.1 基础介绍和用法

在 C++17 之前,如果你想在一个变量中存储多种可能的类型,通常会使用 unionvoid* 指针。然而,这些方法都有明显的缺点。使用 union 时,类型信息会丢失,使得代码容易出错。而 void* 指针则需要手动进行类型转换和内存管理,容易导致内存泄漏或未定义的行为。

std::variant(变体)作为一种更安全、更方便的多类型容器,应运而生。你可以把它看作是一个可以存储多种类型中的任一种的类型安全的容器。下面是一个基本用法的例子:

#include <variant>
#include <iostream>
int main() {
    std::variant<int, double, std::string> v1 = 42;
    std::variant<int, double, std::string> v2 = 3.14;
    std::variant<int, double, std::string> v3 = "hello";
    
    // 访问存储的值(不安全,需确保类型正确)
    std::cout << std::get<int>(v1) << std::endl;
    
    // 安全地访问存储的值
    if (auto pval = std::get_if<int>(&v1)) {
        std::cout << *pval << std::endl;
    }
    
    return 0;
}

在这个例子中,我们定义了三个 std::variant 变量,分别存储了 intdoublestd::string 类型的值。

2.2 与 unionvoid* 的比较

union void* std::variant
类型安全
自动内存管理
运行时类型信息
性能 ⚖️ ⚖️ ⚖️
代码可读性

相比之下,std::variant 提供了一种类型安全、自动管理内存和运行时获取类型信息的方式,显著提高了代码质量和可维护性。这就像是你拥有了一个瑞士军刀,但每把刀片都刻有明确的标签,你总是知道该使用哪一把。

2.3 std::variant 的局限性

尽管 std::variant 非常强大,但它并不是万能的。它的一个主要限制是,虽然它可以存储多种类型,但在任何给定时间点,它只能存储其中一种。这就像是一个变色的蜥蜴,虽然它可以变成多种颜色,但一次只能是一种。

这就引出了一个问题:当你拿到一个 std::variant 对象时,如何知道它当前存储了哪种类型的值?这是下一章节 std::visit 登场的时候。

当然,你可以使用 std::holds_alternativestd::get_if 进行手动检查,但这样做的代码通常既繁琐又容易出错。正如 Herbert Schildt 在其著作《C++ 完全手册》中所说,简洁性往往是高效代码的关键。

在深入探讨 std::visit 之前,了解 std::variant 的局限性是非常重要的。这并不是因为 std::variant 是一个不好的工具,恰恰相反,它是一个极其有用的构造。然而,正如 Robert C. Martin 在他的著作 “Clean Code” 中提到的,每一个工具都有其适用场景,以及不适用的场景。所以,让我们先了解一下 std::variant 在什么情况下可能让你陷入困境。

3. 类型检查

3.1 需要运行时类型检查

3.1.1 静态类型与动态类型

在 C++ 这样的静态类型(Static Typing)语言中,类型信息在编译时就已经确定。然而,当你使用 std::variant(变体)时,你实际上是在模拟动态类型(Dynamic Typing)的行为。这意味着你需要在运行时去判断它究竟存储了哪种类型的对象。

这和编程中的“迪米特法则”(Law of Demeter)有点矛盾。这一法则告诉我们,一个对象应该对其他对象有最少的了解。当你不得不去检查一个 std::variant 所存储的具体类型时,你实际上是在违反这一原则。

3.1.2 手动类型检查的风险

C++ 提供了 std::holds_alternativestd::get 等函数,用于检查和提取 std::variant 中存储的类型。这种做法虽然有效,但是很容易出错。

std::variant<int, double, std::string> v = 42;
if (std::holds_alternative<int>(v)) {
    int value = std::get<int>(v);  // 安全
} else if (std::holds_alternative<double>(v)) {
    double value = std::get<double>(v);  // 运行时错误!
}

如果你不小心用了错误的类型去访问 std::variant,会抛出一个 std::bad_variant_access 异常。这种情况下,你不得不依赖运行时错误检查,这无疑增加了代码的复杂性。

3.2 如何手动进行类型检查

手动类型检查通常涉及使用 std::holds_alternativestd::get,或者更糟糕的是,使用 std::get_if。这些方法都有其适用场合,但也都有明显的缺点。

方法 优点 缺点
std::holds_alternative 简单、直观 不能提取值
std::get 可以直接提取值 类型错误会抛出异常
std::get_if 可以检查和提取值,不会抛出异常 返回指针,需要额外的空指针检查

考虑到这些局限性,一个更加统一和安全的解决方案就显得非常有用。这也正是 std::visit 的用武之地。

当你面对一个复杂的问题时,心里可能会产生一种想逃避的冲动。这时,最好的方法是将问题拆分成更小、更易管理的部分。这也是 std::visit 的核心思想:它允许你将复杂的类型检查和数据提取问题分解为更简单、更易于管理的部分。

4. std::visit 简介

std::visit 是 C++17 中引入的一个工具,用于访问和操作存储在 std::variant 类型中的数据。std::variant 是一种类型安全的联合体,可以存储固定集合中的任何类型,但在任何给定时间只能持有这些类型中的一个。

4.1 基本接口

std::visit 的基本接口如下:

template<class Visitor, class... Variants>
constexpr visit(Visitor&& vis, Variants&&... vars);
  • Visitor:一个可调用对象,它应该能够接受 Variants 中每种类型的值。它通常是一个重载了 operator() 的结构或类。
  • Variants:一个或多个 std::variant 类型的对象。

4.2 使用方式

使用 std::visit 的一个典型方式是定义一个结构体或类,该结构体或类重载了针对 std::variant 可能持有的每种类型的 operator() 方法。然后将这个结构体或类的实例以及 std::variant 对象传递给 std::visitstd::visit 将自动调用与 std::variant 当前存储的值类型相匹配的重载方法。

5. std::visit 的工作原理

std::visit 的底层原理涉及几个关键概念,包括类型擦除、类型恢复和函数重载解析。这是一个相对复杂的机制,尤其是在涉及模板和变参模板时。以下是 std::visit 的底层工作原理的概述:

  1. 类型擦除std::variant 是一个类型擦除容器,它可以存储一定范围内的不同类型的对象。它内部通常有一个联合体来存储数据和一个标记来表示当前存储的类型。
  2. 访问存储的值:当 std::visit 被调用时,它首先需要确定 std::variant 当前存储的具体类型。这是通过检查内部的类型标记完成的。
  3. 函数模板实例化std::visit 接受一个可调用对象和一个或多个 std::variant 对象。这个可调用对象通常是一个重载的函数对象或 lambda 表达式,其具有多个重载以处理不同的类型。编译器会为这些重载生成函数模板实例。
  4. 类型恢复和函数调用:一旦确定了 std::variant 中的类型,std::visit 通过生成的模板代码来“恢复”此类型,并调用与该类型匹配的函数重载。如果有多个 std::variant 参数,std::visit 将处理所有组合的可能性,并调用适当的重载。
  5. 编译时多态:这一切都在编译时发生。编译器生成适用于所有可能的类型组合的代码。因此,std::visit 实现了一种编译时的多态,而不是运行时多态(如虚函数)。
  6. 效率和优化:由于大部分工作在编译时完成,std::visit 通常比运行时类型检查(如动态类型转换)更高效。编译器可以优化函数调用,尤其是在可预测的分支和内联函数的情况下。

综上所述,std::visit 的核心在于它能够在编译时处理多态性,允许编译器生成处理 std::variant 中所有可能类型的代码。这种方法确保了类型安全,并允许进行高效的代码优化。

6. 如何优雅地使用 std::visit

6.1 使用泛型 lambda 表达式

std::visit 允许你传入一个可调用对象(callable object),通常是一个 lambda 表达式。现代 C++ 提供了一种特殊的 lambda 表达式,称为泛型 lambda 表达式(generic lambda)。

6.1.1 什么是泛型 lambda?

泛型 lambda 是一个使用 auto 关键字作为参数类型的 lambda 表达式。这意味着 lambda 可以接受任何类型的参数,并在函数体内进行处理。

auto generic_lambda = [](auto x) {
    // do something with x
};

这种灵活性在处理 std::variant 时尤为有用,因为你可能需要根据多种可能的类型来编写逻辑。

6.2 使用 if constexpr 和类型萃取

编程就像是一场高级的拼图游戏。你需要一种机制来判断哪块拼图适用于当前的情况。在 std::visit 的上下文中,这通常是通过 if constexpr 和类型萃取(type traits)来完成的。

6.2.1 if constexpr 的威力

if constexpr 是 C++17 引入的一种编译时 if 语句,它允许在编译时进行条件判断。这意味着编译器会根据条件来优化生成的代码,这通常会带来更高的性能。

使用 if constexpr,你可以在一个统一的代码块中处理多种类型,而无需使用多个繁琐的 if-else 语句。这不仅让代码看起来更简洁,而且更易于维护。

6.2.2 类型萃取:认识你的类型

类型萃取(Type Traits)是 C++11 引入的一组模板,用于在编译时获取类型的属性。例如,std::is_same_v 可以告诉你 T1T2 是否是同一种类型。

通过结合 if constexpr 和类型萃取,你可以写出高度灵活且类型安全的代码。这也是 std::visit 能发挥最大威力的地方。

6.3 综合应用:泛型 lambda 与类型判断

现在,让我们把这些元素融合到一起,看看如何优雅地使用 std::visit

std::variant<int, double, std::string> v = "hello";
std::visit([](auto&& arg) {
    using T = std::decay_t<decltype(arg)>;
    if constexpr (std::is_same_v<T, int>) {
        std::cout << "int: " << arg << std::endl;
    } else if constexpr (std::is_same_v<T, double>) {
        std::cout << "double: " << arg << std::endl;
    } else {
        static_assert(std::is_same_v<T, std::string>);
        std::cout << "string: " << arg << std::endl;
    }
}, v);

这里,我们使用了泛型 lambda 来接受任何类型的 arg,然后用 if constexpr 和类型萃取来确定 arg 的实际类型,并据此执行相应的操作。

6.4 std::visit和访问者 模式

一个简单的 std::visit 使用示例。在这个例子中,我将使用 std::variant 来存储不同类型的数据,并展示如何使用 std::visit 以类型安全的方式访问和处理这些数据。

假设我们有一个 std::variant,它可以存储一个 int、一个 double 或一个 std::string 类型的值。我们将编写一个访问者函数对象,这个对象会根据 std::variant 当前存储的类型执行不同的操作。

#include <iostream>
#include <variant>
#include <string>
#include <functional>
// 定义 variant 类型
using MyVariant = std::variant<int, double, std::string>;
// 访问者函数对象
struct VariantVisitor {
    void operator()(int i) const {
        std::cout << "处理 int: " << i << std::endl;
    }
    void operator()(double d) const {
        std::cout << "处理 double: " << d << std::endl;
    }
    void operator()(const std::string& s) const {
        std::cout << "处理 string: " << s << std::endl;
    }
};
int main() {
    MyVariant v1 = 10;        // v1 存储 int
    MyVariant v2 = 3.14;      // v2 存储 double
    MyVariant v3 = "hello";   // v3 存储 string
    std::visit(VariantVisitor(), v1); // 输出: 处理 int: 10
    std::visit(VariantVisitor(), v2); // 输出: 处理 double: 3.14
    std::visit(VariantVisitor(), v3); // 输出: 处理 string: hello
    return 0;
}

在这个例子中:

  • 我们定义了一个 std::variant 类型 MyVariant,它可以存储 intdoublestd::string
  • VariantVisitor 是一个重载了 operator() 的结构体,对每种可能的类型提供了一个处理方法。
  • main 函数中,我们创建了三个 MyVariant 实例,分别存储不同的类型。
  • 使用 std::visit 调用 VariantVisitor 实例,它会自动选择并调用与 variant 当前存储的类型相匹配的重载函数。

这个例子展示了 std::visit 如何提供一种类型安全、灵活的方式来处理存储在 std::variant 中的不同类型的数据。

6.4.1 进一步研究与访问者模式的兼容

如果您想要在 operator() 中添加额外的参数,std::visit 本身不会直接支持这种用法,因为 std::visit 期望的可调用对象的参数必须与传递给它的 std::variant 类型匹配。不过,您可以通过一些技巧来实现这个功能。

一种常用的方法是使用 lambda 表达式或绑定器(如 std::bind)来封装您的访问者对象和额外的参数。这里有一个简单的示例说明如何做到这一点:

#include <variant>
#include <iostream>
#include <functional>
struct MyVisitor {
    void operator()(int i, const std::string& extra) const {
        std::cout << "Int: " << i << ", Extra: " << extra << '\n';
    }
    void operator()(float f, const std::string& extra) const {
        std::cout << "Float: " << f << ", Extra: " << extra << '\n';
    }
    void operator()(const std::string& s, const std::string& extra) const {
        std::cout << "String: " << s << ", Extra: " << extra << '\n';
    }
};
int main() {
    std::variant<int, float, std::string> v;
    std::string extraInfo = "Some extra information";
    v = 12;
    std::visit([&](auto&& arg){ MyVisitor{}(arg, extraInfo); }, v);
    v = 3.14f;
    std::visit([&](auto&& arg){ MyVisitor{}(arg, extraInfo); }, v);
    v = "Hello World";
    std::visit([&](auto&& arg){ MyVisitor{}(arg, extraInfo); }, v);
}

在这个示例中,我们定义了一个 MyVisitor 结构体,其中 operator() 接受两个参数。为了与 std::visit 配合使用,我们在 std::visit 的调用中使用了一个 lambda 表达式。这个 lambda 表达式接受 std::variant 的当前值作为其第一个参数,并将额外的参数(如 extraInfo 字符串)传递给 MyVisitoroperator()。这种方法允许您向 operator() 传递额外的参数,同时仍然利用 std::visit 的能力。


在 C++ 中,[&](auto&& arg){ MyVisitor{}(arg, extraInfo); } 是一个 lambda 表达式,用于创建一个匿名函数。这个特定的 lambda 表达式用于 std::visit 调用中,以便将 std::variant 的值和额外的参数一起传递给 MyVisitor 类的 operator()。我将为您详细解释每个部分的含义:

  1. [&] 捕获子句:这部分定义了 lambda 表达式捕获外部作用域中的变量的方式。在这种情况下,& 表示以引用方式捕获所有外部变量(在这个例子中,主要是 extraInfo)。这意味着 lambda 表达式内部可以访问并使用外部作用域中定义的 extraInfo 变量。
  2. (auto&& arg) 参数列表:这表示 lambda 接受一个名为 arg 的参数,auto&& 是一个通用引用,它可以接受任何类型的参数。在 std::visit 的上下文中,这个参数将是 std::variant 中当前存储的值。
  3. 函数体{ MyVisitor{}(arg, extraInfo); } 是 lambda 表达式的函数体。在这里,它创建了 MyVisitor 类的一个临时实例,并调用其 operator(),传递两个参数:arg(从 std::variant 中得到的值)和 extraInfo(从外部作用域捕获的额外信息)。

综合起来,当这个 lambda 表达式被 std::visit 调用时,它会根据 std::variant 当前存储的类型将相应的值作为 arg 传递给 MyVisitoroperator(),同时携带一个额外的参数 extraInfo。这允许 MyVisitor 的方法根据当前的 variant 类型和额外的信息执行相应的操作。

7. 使用 std::visit 的优缺点

7.1 优点

7.1.1 代码简洁

使用 std::visit 可以让你的代码变得更加简洁和组织良好。这正是Bruce Eckel在《Thinking in C++》中所强调的,即“代码的可读性和维护性应当是编程中的首要任务”。

考虑一个没有使用 std::visit 的例子,你可能会这样写:

if (std::holds_alternative<int>(v)) {
    // 处理 int 类型
} else if (std::holds_alternative<double>(v)) {
    // 处理 double 类型
} else if (std::holds_alternative<std::string>(v)) {
    // 处理 std::string 类型
}

而使用 std::visit,这些 if-else 语句可以被优雅地替换为一个泛型 lambda 表达式:

std::visit([](auto&& arg) {
    // 统一处理逻辑
}, v);

这种简洁性对于代码的组织和可读性有着明显的优势。简单来说,简洁的代码更容易被理解和维护。

7.1.2 类型安全

std::visit 还具有类型安全(Type Safety)的优点。这意味着编译器将在编译阶段检查类型错误,减少了运行时错误的风险。这与 C++ 的核心原则一致,即“让错误尽早地暴露出来”。

7.1.3 扩展性

std::visit 的另一个优点是扩展性(Extensibility)。如果 std::variant 添加了新的类型,你只需要更新 std::visit 的访问器函数,而无需改动其他代码。

7.2 缺点

7.2.1 性能影响

尽管 std::visit 提供了许多优势,但它并非没有代价。其中之一就是潜在的性能影响。由于 std::visit 需要进行运行时类型检查,这可能会引入一定的开销。

然而,现代编译器通常会进行优化,使这种开销最小化。实际上,许多情况下,使用 std::visit 造成的性能损失是可以接受的。

7.2.2 模板代码膨胀

std::visit 是模板函数,这意味着每一种类型组合都可能生成新的实例代码,导致所谓的“模板代码膨胀”(Template Bloat)。

方法 代码简洁性 类型安全性 扩展性 性能影响 代码膨胀
手动类型检查 (if-else)
std::visit 可变

8. 实战:应用案例

在这一章节,我们将从实际应用的角度出发,探讨如何有效地利用 std::visit(标准访问器)来简化和优化代码。你可能听过这句名言:“实践出真知”。在编程世界中,这同样适用。理解一个概念的最好方式就是将其应用于实际问题。

8.1 用于配置管理

假设你正在开发一个大型软件项目,其中包含多种类型的配置选项,例如整数、浮点数、字符串等。你可能会将这些配置选项存储在一个 std::map> 中。

8.1.1 动态解析配置

在不使用 std::visit 的情况下,你可能需要使用一系列 if-else 语句和 std::holds_alternative(类型检查)来手动解析这些配置。

std::map<std::string, std::variant<int, double, std::string>> config;
// ... 填充配置
for (const auto& [key, value] : config) {
    if (std::holds_alternative<int>(value)) {
        // 处理 int 类型
    } else if (std::holds_alternative<double>(value)) {
        // 处理 double 类型
    } else {
        // 处理 string 类型
    }
}

这样做虽然可行,但不够优雅。每次添加或删除类型时,你都需要更新这个大型 if-else 语句。

8.1.2 利用 std::visit 简化代码

通过使用 std::visit,你可以将这个复杂的逻辑简化为一个简洁、可维护的代码块。

std::map<std::string, std::variant<int, double, std::string>> config;
// ... 填充配置
for (const auto& [key, value] : config) {
    std::visit([](auto&& arg) {
        using T = std::decay_t<decltype(arg)>;
        // 这里进行类型相关的操作
    }, value);
}

这样,每当你需要添加或删除一个类型时,只需更新 lambda 函数体内的内容即可。

8.2 在游戏开发中的应用

假设你正在开发一个角色扮演游戏(RPG),其中的物品(如武器、药水、装备等)可以用一个 std::variant 类型来表示。

8.2.1 角色与物品交互

在游戏中,角色与这些物品的交互方式各不相同。例如,拾取一个武器会增加攻击力,而使用一个药水则会恢复生命值。在不使用 std::visit 的情况下,你可能需要写出大量的代码来处理这些交互。

8.2.2 std::visit 的高效应用

使用 std::visit,你可以将所有的交互逻辑集中在一个地方,使得代码更容易维护。

std::visit([](auto&& item) {
    using T = std::decay_t<decltype(item)>;
    // 根据 T 的类型进行相应的角色与物品的交互
}, pickedItem);

你可能注意到了,使用 std::visit 和泛型 lambda 表达式(Generic Lambda Expressions)可以极大地简化代码,同时还能保持高度的灵活性和可维护性。

方法 灵活性 可维护性 代码复杂性
手动类型检查
使用 std::visit

8.3 状态机模型

在嵌入式系统、网络协议或复杂的用户界面中,状态机(State Machines)是一种常见的设计模式。这些状态机可能会有多种状态和转换逻辑。

8.3.1 繁琐的状态管理

在传统的设计中,状态通常由枚举(Enums)或整数常量表示,而状态转换则通过一系列复杂的 if-elseswitch-case 语句来管理。

8.3.2 std::visit 的优雅应用

通过使用 std::variant 来表示不同的状态,以及使用 std::visit 来处理状态转换,你可以将整个状态机模型简化为一个结构化、易于维护的系统。

std::variant<IdleState, RunningState, ErrorState> currentState;
// ... 更新状态
std::visit([](auto&& state) {
    using T = std::decay_t<decltype(state)>;
    // 根据 T 的类型进行相应的状态转换
}, currentState);

通过这种方式,添加或删除状态变得异常简单,只需修改一处代码即可。

结语

在我们的编程学习之旅中,理解是我们迈向更高层次的重要一步。然而,掌握新技能、新理念,始终需要时间和坚持。从心理学的角度看,学习往往伴随着不断的试错和调整,这就像是我们的大脑在逐渐优化其解决问题的“算法”。

这就是为什么当我们遇到错误,我们应该将其视为学习和进步的机会,而不仅仅是困扰。通过理解和解决这些问题,我们不仅可以修复当前的代码,更可以提升我们的编程能力,防止在未来的项目中犯相同的错误。

我鼓励大家积极参与进来,不断提升自己的编程技术。无论你是初学者还是有经验的开发者,我希望我的博客能对你的学习之路有所帮助。如果你觉得这篇文章有用,不妨点击收藏,或者留下你的评论分享你的见解和经验,也欢迎你对我博客的内容提出建议和问题。每一次的点赞、评论、分享和关注都是对我的最大支持,也是对我持续分享和创作的动力。

目录
相关文章
|
1天前
|
编译器 C++ 容器
C++模板的原理及使用
C++模板的原理及使用
|
6天前
|
负载均衡 算法
Dubbo-负载均衡原理解析(1),一个本科渣渣是怎么逆袭从咸鱼到Offer收割机的
Dubbo-负载均衡原理解析(1),一个本科渣渣是怎么逆袭从咸鱼到Offer收割机的
|
6天前
|
Android开发
Flutter完整开发实战详解(六、 深入Widget原理),2024百度Android岗面试真题收录解析
Flutter完整开发实战详解(六、 深入Widget原理),2024百度Android岗面试真题收录解析
|
7天前
|
Web App开发 开发框架 前端开发
Open UI5 前端开发框架配套的 Mock Server 工作原理解析
Open UI5 前端开发框架配套的 Mock Server 工作原理解析
12 0
|
7天前
|
存储 Java Go
Go 语言切片如何扩容?(全面解析原理和过程)
Go 语言切片如何扩容?(全面解析原理和过程)
16 2
|
7天前
|
机器学习/深度学习 存储 算法
卷积神经网络(CNN)的数学原理解析
卷积神经网络(CNN)的数学原理解析
36 1
卷积神经网络(CNN)的数学原理解析
|
1天前
|
编译器 C++
【C++】类和对象(下)
【C++】类和对象(下)
|
1天前
|
编译器 C++
【C++】类和对象(中)(2)
【C++】类和对象(中)(2)
|
1天前
|
存储 编译器 C++
【C++】类和对象(中)(1)
【C++】类和对象(中)(1)
|
1天前
|
存储 编译器 C语言
【C++】类和对象(上)
【C++】类和对象(上)

推荐镜像

更多