为什么写这篇文章?
在开发中,常常需要比较软件版本号(如 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++11到C++23
以下是实现,我和各位一起探讨下这其中的工程细节。
实现之前,贴个表格,说明单个字符串进行比较是不行的:
| 比较 | 字符串结果 | 实际期望 |
|---|---|---|
| “1.10” >= “1.2” | false | true |
| “1.0.0” >= “1” | false | true |
| “2.0” >= “1.999” | true | true |
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 ×

1147

被折叠的 条评论
为什么被折叠?



