使用C++17 std::variant实现安全的多态结构体解析器

在现代C++中,std::variant 提供了一种类型安全的方式来保存多种可能的值,类似于联合体,但具有更强的类型检查和更易用的接口。利用 std::variant 可以轻松实现一个解析器,能够将输入字符串解析为不同的结构体类型,并保证在处理过程中不会出现类型错误。下面给出一个完整的示例,演示如何使用 std::variantstd::visit、以及结构体模板化来构建一个简易的多态解析器。

1. 定义多种数据结构

#include <iostream>
#include <variant>
#include <string>
#include <sstream>
#include <iomanip>
#include <vector>

struct User {
    std::string name;
    int age;
};

struct Product {
    std::string name;
    double price;
};

struct Order {
    std::string id;
    std::vector<std::string> items;
};

2. 为每种结构体实现解析函数

bool parseUser(const std::string& line, User& out) {
    std::istringstream ss(line);
    return (ss >> out.name >> out.age) && ss.eof();
}

bool parseProduct(const std::string& line, Product& out) {
    std::istringstream ss(line);
    return (ss >> out.name >> out.price) && ss.eof();
}

bool parseOrder(const std::string& line, Order& out) {
    std::istringstream ss(line);
    std::string item;
    if (!(ss >> out.id)) return false;
    while (ss >> item) out.items.push_back(item);
    return !out.items.empty();
}

3. 定义 std::variant 以及 std::visit 访问器

using Item = std::variant<User, Product, Order>;

struct ItemPrinter {
    void operator()(const User& u) const {
        std::cout << std::left << std::setw(10) << "User" << " | Name: " << u.name << ", Age: " << u.age << '\n';
    }
    void operator()(const Product& p) const {
        std::cout << std::left << std::setw(10) << "Product" << " | Name: " << p.name << ", Price: $" << std::fixed << std::setprecision(2) << p.price << '\n';
    }
    void operator()(const Order& o) const {
        std::cout << std::left << std::setw(10) << "Order" << " | ID: " << o.id << ", Items: ";
        for (size_t i = 0; i < o.items.size(); ++i) {
            std::cout << o.items[i];
            if (i + 1 < o.items.size()) std::cout << ", ";
        }
        std::cout << '\n';
    }
};

4. 解析并处理多行输入

Item parseLine(const std::string& line) {
    User u;
    if (parseUser(line, u)) return u;

    Product p;
    if (parseProduct(line, p)) return p;

    Order o;
    if (parseOrder(line, o)) return o;

    throw std::runtime_error("未知的数据格式: " + line);
}

5. 主程序

int main() {
    std::vector<std::string> inputs = {
        "Alice 28",
        "Widget 19.99",
        "ORD123 ItemA ItemB ItemC",
        "Bob 35",
        "Gadget 49.5",
        "ORD124 ItemX ItemY"
    };

    std::vector <Item> items;
    for (const auto& line : inputs) {
        try {
            items.push_back(parseLine(line));
        } catch (const std::exception& e) {
            std::cerr << "解析错误: " << e.what() << '\n';
        }
    }

    std::cout << "=== 解析结果 ===\n";
    for (const auto& item : items) {
        std::visit(ItemPrinter{}, item);
    }

    return 0;
}

6. 运行结果

=== 解析结果 ===
User      | Name: Alice, Age: 28
Product   | Name: Widget, Price: $19.99
Order     | ID: ORD123, Items: ItemA, ItemB, ItemC
User      | Name: Bob, Age: 35
Product   | Name: Gadget, Price: $49.50
Order     | ID: ORD124, Items: ItemX, ItemY

7. 代码说明

  1. 类型安全std::variant 确保每个 Item 必须是 UserProductOrder 之一,编译器会在访问时检查类型,避免了传统 void* 的危险。
  2. 简洁的访问std::visitItemPrinter 结合,实现了对不同类型的统一打印逻辑,代码可读性大幅提升。
  3. 易于扩展:只需添加新的结构体、解析函数以及对应的访问器实现,整体框架不需要改动。
  4. 错误处理:如果一行无法匹配任何已知格式,parseLine 抛出异常,主程序捕获并报告错误,保证程序不会在解析失败时崩溃。

8. 进阶改进

  • **使用 `std::optional `** 代替抛异常,在解析失败时返回空值,更符合现代 C++ 的异常友好设计。
  • 正则表达式:为复杂数据结构使用正则表达式提取字段,进一步提升解析灵活性。
  • 自定义访问器:利用 std::variantvisit 支持多重重载,直接在 ItemPrinter 中实现多态打印逻辑,而无需显式 if-elseswitch

通过上述示例,你可以快速构建一个类型安全且易维护的多态结构体解析器。std::variantstd::visit 的组合,使得 C++ 代码既保持了强类型优势,又拥有了类似脚本语言的数据处理灵活性。

发表评论