版本号比较翻车实录:从 “1.19 < 1.2” 到全标准兼容的C++解法

为什么写这篇文章?
在开发中,常常需要比较软件版本号(如 1.2.3 vs 1.2.4),但看似简单的功能,却隐藏着许多陷阱:

  • 字符串直接比较会出错(“1.10” < “1.2” );
  • 忽略 v 前缀、支持任意段数、防止整数溢出、兼容不同 C++ 标准……

1. 目的

本文将实现一个版本号比较工具,包含以下方面:

  • 能处理任意长度(1、1.2、1.2.3.4.5 都行)
  • 自动忽略 v 或 V 前缀
  • 防止整数溢出、非法字符
  • 兼容:兼容C++11C++23

以下是实现,我和各位一起探讨下这其中的工程细节。
实现之前,贴个表格,说明单个字符串进行比较是不行的:

比较字符串结果实际期望
“1.10” >= “1.2”falsetrue
“1.0.0” >= “1”falsetrue
“2.0” >= “1.999”truetrue

2. 实现思路

2.1 自动检测C++标准

相关头文件:

#include <iostream>
#include <vector>
#include <string>
#include <sstream>
#include <algorithm>
#include <stdexcept>
#include <cstring>

通过__cplusplus宏,动态启用最优特性:

#if defined(__cplusplus) && __cplusplus >= 202002L
#define VERSION_USE_STRING_VIEW 1
#define VERSION_USE_FROM_CHARS 1
#elif defined(__cplusplus) && __cplusplus >= 201703L
#define VERSION_USE_STRING_VIEW 1
#define VERSION_USE_FROM_CHARS 0
#elif defined(__cplusplus) && __cplusplus >= 201103L
#define VERSION_USE_STRING_VIEW 0
#define VERSION_USE_FROM_CHARS 0
#else
#error "C++11 or later is required"
#endif

#if VERSION_USE_STRING_VIEW
#include <string_view>
using version_string_type = std::string_view;
#else
using version_string_type = const std::string&;
#endif
2.2 安全的数字校验

避免使用 isdigit(有符号 char 可能导致未定义行为)
仅允许 0-9,拒绝全角数字、locale 依赖

inline bool is_all_digits(version_string_type s)
{
    if (s.empty())
    {
        return false;
    }

#if VERSION_USE_STRING_VIEW
    return std::all_of(s.begin(), s.end(), [](char c) {
        return c >= '0' && c <= '9';
    });
#else
    for (size_t i = 0; i < s.size(); ++i)
    {
        if (s[i] < '0' || s[i] > '9')
        {
            return false;
        }
    }
    return true;
#endif
}
2.3 安全整数解析,该步很重要

使用std::stoi会抛异常,而使用atoi不检查溢出。
因为,需实现自定义解决方案:分层实现safe_stoi

#if VERSION_USE_FROM_CHARS
inline bool safe_stoi(version_string_type sv, int& out)
{
    const char* begin = sv.data();
    const char* end = begin + sv.size();
    auto result = std::from_chars(begin, end, out);
    return result.ec == std::errc{} && result.ptr == end;
}
#else
inline bool safe_stoi(version_string_type s, int& out)
{
#if VERSION_USE_STRING_VIEW
    // 临时转 string(C++17 无 from_chars 时的回退)
    std::string temp(s);
    try {
        size_t pos;
        out = std::stoi(temp, &pos);
        return pos == temp.size();
    } catch (...) {
        return false;
    }
#else
    if (s.empty()) return false;
    char* end;
    errno = 0;
    const char* begin = s.c_str();
    long val = std::strtol(begin, &end, 10);

    if (errno == ERANGE)
    {
        return false;
    }

    // 有非数字字符
    if (end == begin || end != begin + s.size())
    {
        return false;
    }
    // 溢出
    if (val < INT_MIN || val > INT_MAX)
    {
        return false;
    }
    out = static_cast<int>(val);
    return true;
#endif
}
#endif

tips : 必须检查 errno == ERANGE!否则在 Windows(long == int)上会漏掉溢出。

2.4 版本解析与比较
  • 自动跳过 v/V 前缀
  • 手动 split(C++17+)或 stringstream(C++11)
  • 短版本自动补 0(“1.2” → {1,2,0,0,…})
  • 利用 std::vector 的字典序比较(p1 >= p2)
std::vector<int> parse_version(version_string_type input)
{
    // 决定起始位置(跳过 v/V)
    size_t offset = 0;
    if (!input.empty() && (input[0] == 'v' || input[0] == 'V'))
    {
        offset = 1;
    }

#if VERSION_USE_STRING_VIEW
    std::string_view ver = input.substr(offset);
#else
    // 创建可修改副本
    std::string ver = input.substr(offset);
#endif

    std::vector<int> parts;

#if VERSION_USE_STRING_VIEW
    size_t start = 0;
    while (start < ver.size())
    {
        size_t end = ver.find('.', start);
        size_t len = (end == std::string_view::npos) ? ver.size() - start : end - start;
        std::string_view part = ver.substr(start, len);

        if (!is_all_digits(part))
        {
            return {};
        }
        int num;
        if (!safe_stoi(part, num))
        {
            return {};
        }
        parts.push_back(num);

        if (end == std::string_view::npos)
            break;

        start = end + 1;
    }
#else
    std::istringstream iss(ver);
    std::string part;
    while (std::getline(iss, part, '.'))
    {
        if (!is_all_digits(part))
        {
            return {};
        }
        int num;
        if (!safe_stoi(part, num))
        {
            return {};
        }
        parts.push_back(num);
    }
#endif

    return parts;
}

3. 随便写点什么

版本比较适用场景

  • 软件版本检查
  • 工程文件兼容性控制
  • 插件/模块版本依赖管理
  • 构建系统中的版本校验

版本号比较,看起来 trivial,实则暗藏玄机,做起来有不少细节。
从字符串陷阱,到整数溢出,再到 C++ 标准兼容——每一步都值得认真对待。

4. 完整示例:可直接复制使用

类名:VersionCompareTool.h
创建一个文件,起这个类名,将以下代码复制过去。

#ifndef VERSIONCOMPARETOOL_H
#define VERSIONCOMPARETOOL_H

#include <vector>
#include <string>
#include <sstream>
#include <algorithm>
#include <stdexcept>
#include <cstring>

#if defined(__cplusplus) && __cplusplus >= 202002L
#define VERSION_USE_STRING_VIEW 1
#define VERSION_USE_FROM_CHARS 1
#elif defined(__cplusplus) && __cplusplus >= 201703L
#define VERSION_USE_STRING_VIEW 1
#define VERSION_USE_FROM_CHARS 0
#elif defined(__cplusplus) && __cplusplus >= 201103L
#define VERSION_USE_STRING_VIEW 0
#define VERSION_USE_FROM_CHARS 0
#else
#error "C++11 or later is required"
#endif

#if VERSION_USE_STRING_VIEW
#include <string_view>
using version_string_type = std::string_view;
#else
using version_string_type = const std::string&;
#endif

class VersionCompareTool
{
public:
    // 比较 v1 == v2
    static bool equal(version_string_type v1, version_string_type v2)
    {
        auto p1 = parse_version(v1);
        auto p2 = parse_version(v2);
        if (p1.empty() || p2.empty())
        {
            throw std::invalid_argument("Invalid version string");
        }
        size_t max_size = std::max(p1.size(), p2.size());
        p1.resize(max_size, 0);
        p2.resize(max_size, 0);
        // std::vector 支持字典序比较(C++11 起)
        return p1 == p2;
    }

    // 比较 v1 >= v2
    static bool greaterOrEqual(version_string_type v1, version_string_type v2)
    {
        auto p1 = parse_version(v1);
        auto p2 = parse_version(v2);
        if (p1.empty() || p2.empty())
        {
            throw std::invalid_argument("Invalid version string");
        }
        size_t max_size = std::max(p1.size(), p2.size());
        p1.resize(max_size, 0);
        p2.resize(max_size, 0);
        // std::vector 支持字典序比较(C++11 起)
        return p1 >= p2;
    }

    // 比较 v1 > v2
    static bool greaterThan(version_string_type v1, version_string_type v2)
    {
        return greaterOrEqual(v1, v2) && !equal(v1, v2);
    }

    // 比较 v1 < v2
    static bool lessThan(version_string_type v1, version_string_type v2)
    {
        return !greaterOrEqual(v1, v2);
    }

    // 比较 v1 <= v2
    static bool lessOrEqual(version_string_type v1, version_string_type v2)
    {
        return !greaterThan(v1, v2);
    }

private:
    // 安全的数字判断
    static bool is_all_digits(version_string_type s)
    {
        if (s.empty())
        {
            return false;
        }

#if VERSION_USE_STRING_VIEW
        return std::all_of(s.begin(), s.end(), [](char c) {
            return c >= '0' && c <= '9';
        });
#else
        for (size_t i = 0; i < s.size(); ++i)
        {
            if (s[i] < '0' || s[i] > '9')
            {
                return false;
            }
        }
        return true;
#endif
    }

// 安全的字符串转整数
#if VERSION_USE_FROM_CHARS
    static bool safe_stoi(version_string_type sv, int& out)
    {
        const char* begin = sv.data();
        const char* end = begin + sv.size();
        auto result = std::from_chars(begin, end, out);
        return result.ec == std::errc{} && result.ptr == end;
    }
#else
    static bool safe_stoi(version_string_type s, int& out)
    {
#if VERSION_USE_STRING_VIEW
        // 临时转 string(C++17 无 from_chars 时的回退)
        std::string temp(s);
        try {
            size_t pos;
            out = std::stoi(temp, &pos);
            return pos == temp.size();
        } catch (...) {
            return false;
        }
#else
        if (s.empty()) return false;
        char* end;
        errno = 0;
        const char* begin = s.c_str();
        long val = std::strtol(begin, &end, 10);

        if (errno == ERANGE)
        {
            return false;
        }

        // 有非数字字符
        if (end == begin || end != begin + s.size())
        {
            return false;
        }
        // 溢出
        if (val < INT_MIN || val > INT_MAX)
        {
            return false;
        }
        out = static_cast<int>(val);
        return true;
#endif
    }
#endif

    // 版本解析
    static std::vector<int> parse_version(version_string_type input)
    {
        // 决定起始位置(跳过 v/V)
        size_t offset = 0;
        if (!input.empty() && (input[0] == 'v' || input[0] == 'V'))
        {
            offset = 1;
        }

#if VERSION_USE_STRING_VIEW
        std::string_view ver = input.substr(offset);
#else
        // 创建可修改副本
        std::string ver = input.substr(offset);
#endif

        std::vector<int> parts;

#if VERSION_USE_STRING_VIEW
        size_t start = 0;
        while (start < ver.size())
        {
            size_t end = ver.find('.', start);
            size_t len = (end == std::string_view::npos) ? ver.size() - start : end - start;
            std::string_view part = ver.substr(start, len);

            if (!is_all_digits(part))
            {
                return {};
            }
            int num;
            if (!safe_stoi(part, num))
            {
                return {};
            }
            parts.push_back(num);

            if (end == std::string_view::npos)
                break;

            start = end + 1;
        }
#else
        std::istringstream iss(ver);
        std::string part;
        while (std::getline(iss, part, '.'))
        {
            if (!is_all_digits(part))
            {
                return {};
            }
            int num;
            if (!safe_stoi(part, num))
            {
                return {};
            }
            parts.push_back(num);
        }
#endif

        return parts;
    }
};

#endif // VERSIONCOMPARETOOL_H

5. 调用&&输出

5.1 调用
// 包含头文件
#include <iostream>
#include "VersionCompareTool.h"

// 调用
#if VERSION_USE_FROM_CHARS
    std::cout << "[C++20+] Using std::from_chars and string_view\n";
#elif VERSION_USE_STRING_VIEW
    std::cout << "[C++17] Using string_view\n";
#else
    std::cout << "[C++11/14] Using const std::string&\n";
#endif

    struct TestCase {
        const char* a;
        const char* b;
        bool expected;
    } tests[] = {
        {"1.2.3", "1.2.3", true},
        {"1.2.4", "1.2.3", true},
        {"1.10", "1.2", true},
        {"2.0", "1.999", true},
        {"1.0.0", "1", true},
        {"v1.2.3", "1.2.2", true},
        {"V1.2.3", "1.2.4", false},
        {"10", "9.9.9", false}
    };

    for (const auto& t : tests) {
        bool result = VersionCompareTool::greaterOrEqual(t.a, t.b);
        std::cout << t.a << " >= " << t.b << "\t?"
                  << (result ? "\tYES" : "\tNO")
                  << (result == t.expected ? "\t√" : " \t×") << "\n";
    }
5.2 输出
[C++11/14] Using const std::string&
1.2.3 >= 1.2.3		?	YES		√
1.2.4 >= 1.2.3		?	YES		√
1.10 >= 1.2			?	YES		√
2.0 >= 1.999		?	YES		√
1.0.0 >= 1			?	YES		√
v1.2.3 >= 1.2.2		?	YES		√
V1.2.3 >= 1.2.4		?	NO		√
10 >= 9.9.9			?	YES 	×
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

FreeLikeTheWind.

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值