首页
/ JSON for Modern C++ 中 basic_json::default_object_comparator_t 详解:C++14 透明比较器如何避免键查询的字符串构造

JSON for Modern C++ 中 basic_json::default_object_comparator_t 详解:C++14 透明比较器如何避免键查询的字符串构造

2026-09-06 19:58:01作者:薛曦旖Francesca

default_object_comparator_t 是 nlohmann::basic_json(即 JSON for Modern C++)用来声明 JSON 对象键比较器的默认类型别名。它直接影响对象的键查找、at()/operator[]/value() 等按键访问接口的可用性,以及在 C++14 及以上标准下能否以 std::string_view 等非 std::string 类型直接查询键。本文以该类型的 API 文档为主体,结合仓库源码,讲清楚它的定义、C++14 前后两种取值的差异、实际比较器类型的确定方式,以及它在 API 层如何被消费。

类型定义:C++14 前后两个版本

basic_json 类内部提供了如下的类型别名:

using default_object_comparator_t = std::less<StringType>;  // until C++14

using default_object_comparator_t = std::less<>;            // since C++14

即:在 C++14 之前的编译环境中,它是绑定到字符串键类型 StringType(默认即 std::string)的 std::less<StringType>;自 C++14 起,它被切换为透明比较器 std::less<>(空模板参数形式,Heterogeneous Lookup 比较器)。

在源码 include/nlohmann/json.hpp 中可以看到对应的实现:

/// @brief default object key comparator type
/// The actual object key comparator type (@ref object_comparator_t) may be
/// different.
#if defined(JSON_HAS_CPP_14)
// use of transparent comparator avoids unnecessary repeated construction of temporaries
// in functions involving lookup by key with types other than object_t::key_type (aka. StringType)
using default_object_comparator_t = std::less<>;
#else
using default_object_comparator_t = std::less<StringType>;
#endif

源码注释明确点出了切换动机:“使用透明比较器可以避免在按键查找时反复构造临时对象——尤其是查找键的类型与 object_t::key_type(即 StringType)不同的场景”。

default_object_comparator_tobject_t 的第三个模板实参,因此默认情况下对象容器为:

// until C++14
std::map<std::string, basic_json, std::less<std::string>,
         std::allocator<std::pair<const std::string, basic_json>>>

// since C++14
std::map<std::string, basic_json, std::less<>,
         std::allocator<std::pair<const std::string, basic_json>>>

完整定义见 include/nlohmann/json.hpp

using object_t = ObjectType<StringType,
      basic_json,
      default_object_comparator_t,
      AllocatorType<std::pair<const StringType, basic_json>>>;

透明比较器带来的实际收益

C++14 之前,std::less<std::string> 只能比较两个 std::string。这意味着当用户拿着 std::string_view、字符串字面量(const char*)或宽字符串等去查询对象键时,库必须先把它们构造std::string 再传入查找函数,每一次查找都可能伴随一次字符串分配。

C++14 引入的 std::less<>(heterogeneous 比较器)没有绑定具体类型,其 operator() 接受任意可比较的左右操作数。由于底层容器(std::map)从 C++14 起支持 Heterogeneous Lookup,键查找可以直接使用原始类型与树中已有的 const std::string 键做比较,从而省去临时 std::string 的构造。这正是文档中所述 “Since C++14 a transparent comparator is used which prevents unnecessary string construction when looking up a key in an object” 的含义。

这一收益在库的 API 层被系统性利用:basic_jsonvalue() 等接口依据“比较器是否透明”提供了两组重载——透明比较器版本接受 KeyType && 泛型键(如 string_view),非透明版本只接受 const typename object_t::key_type&。相关重载的 SFINAE 条件可见 include/nlohmann/json.hpp

一个典型的 C++17 用法示例(来自 docs/mkdocs/docs/examples/at__keytype.c++17.cpp)展示了 std::string_view 直接作为键参与 at() 查询与写入:

json object = { {"the good", "il buono"}, {"the bad", "il cattivo"}, {"the ugly", "il brutto"} };

// 使用 string_view 输出键 "the ugly" 对应的元素
std::cout << object.at("the ugly"sv) << '\n';

// 使用 string_view 修改键 "the bad" 对应的元素
object.at("the bad"sv) = "il cattivo";

在 C++14 及以后的编译环境下,由于比较器是透明的,"the ugly"sv 无需先转换为 std::string 即可完成树中查找。

实际生效的比较器:object_comparator_t 的确定逻辑

文档特别指出:default_object_comparator_t 只是默认比较器,实际生效的比较器取决于 object_t,并通过 object_comparator_t 获取:

using object_comparator_t = typename object_t::key_compare;
// or
using object_comparator_t = default_object_comparator_t;

即:如果 object_t 带有 key_compare 成员(如 std::map),则以 typename object_t::key_compare 为准;否则回退到 default_object_comparator_t。源码中的选择逻辑位于 include/nlohmann/detail/meta/type_traits.hpp

// obtains the actual object key comparator
template<typename BasicJsonType>
struct actual_object_comparator
{
    using object_t = typename BasicJsonType::object_t;
    using object_comparator_t = typename BasicJsonType::default_object_comparator_t;
    using type = typename std::conditional<has_key_compare<object_t>::value,
          typename object_t::key_compare, object_comparator_t>::type;
};

template<typename BasicJsonType>
using actual_object_comparator_t = typename actual_object_comparator<BasicJsonType>::type;

basic_jsonobject_comparator_t 正是这样派生出来的(include/nlohmann/json.hpp):

using object_comparator_t = detail::actual_object_comparator_t<basic_json>;

对于默认配置(object_tstd::map),key_compare 就是 default_object_comparator_t 本身,两者一致;但当用户自定义 ObjectType 并指定了自己的比较器时,object_comparator_t 会如实反映该比较器,default_object_comparator_t 则保持为库的默认值不变。

透明性检测与键类型的 SFINAE 约束

库内判断“当前比较器是否透明”的类型特征是 include/nlohmann/detail/meta/type_traits.hpp 中的 is_transparent

// checks whether T has a member named is_transparent
template<typename T>
struct is_transparent : bool_constant<impl::is_transparent<T>()> {};

其实现通过 is_detected<detect_is_transparent, T> 检测比较器类型是否带有 is_transparent 成员(std::less<>is_transparent = truestd::less<std::string> 没有)。

在此基础上,is_usable_as_key_type 等特征(include/nlohmann/detail/meta/type_traits.hpp)进一步约束了哪些类型可以被用作键:

template<typename Comparator, typename ObjectKeyType, typename KeyTypeCVRef, bool RequireTransparentComparator = true,
         bool ExcludeObjectKeyType = RequireTransparentComparator, typename KeyType = uncvref_t<KeyTypeCVRef>>
using is_usable_as_key_type = typename std::conditional<
    is_comparable<Comparator, ObjectKeyType, KeyTypeCVRef>::value
    && !(ExcludeObjectKeyType && std::is_same<KeyType, ObjectKeyType>::value)
    && (!RequireTransparentComparator || is_detected<detect_is_transparent, Comparator>::value)
    && !is_json_pointer<KeyType>::value,
    std::true_type, std::false_type>::type;

其中 is_comparable(同文件 L707-L731)会验证“比较器能否同时以两种参数顺序比较 ObjectKeyTypeKeyType”,这正是 Heterogeneous Lookup 所需的对称可比较性。由此可见:透明比较器不只是一个性能优化,它还决定了泛型键(string_view 等)能否参与键类型相关的编译期检查。

官方示例:直接观察比较器的行为

API 文档给出的示例程序 docs/mkdocs/docs/examples/default_object_comparator_t.cpp 直接实例化 default_object_comparator_t 并比较两个字符串键:

#include <iostream>
#include <nlohmann/json.hpp>

using json = nlohmann::json;

int main()
{
    std::cout << std::boolalpha
              << "one < two : " << json::default_object_comparator_t{}("one", "two") << "\n"
              << "three < four : " << json::default_object_comparator_t{}("three", "four") << std::endl;
}

其输出(docs/mkdocs/docs/examples/default_object_comparator_t.output):

one < two : true
three < four : false

示例印证了两点:

  • default_object_comparator_t 是一个可以直接默认构造并调用的函数对象,语义即字符串的字典序“小于”;
  • 比较按字符的字典序进行("three""four" 比较时 't' > 'f',结果为 false),与 std::map 以键的字典序组织节点的行为一致。

对对象行为的连带影响

比较器是 object_t 的核心组成部分,因此默认比较器也决定了 JSON 对象的多项行为(详见 object_t 文档):

  • 键排序与序列化顺序:由于默认 object_tstd::map 且以 std::less 系列比较器排序,name/value 对按键的字典序存储,dump() 序列化也按此顺序输出。例如 {"b": 1, "a": 2} 会被存储并序列化为 {"a": 2, "b": 1}
  • 对象相等性与遍历顺序无关:对象比较时 name/value 对的顺序不影响结果,{"b": 1, "a": 2}{"a": 2, "b": 1} 相等,符合 RFC 8259 对 JSON 对象“无序”的描述。
  • 重复键的解析行为:键名不唯一时,最终保留哪个值是未指定行为;若要拒绝重复键,文档指向了解析回调方案(见 解析器回调文档 中 “Rejecting duplicate object keys” 一节的示例 docs/mkdocs/docs/examples/reject_duplicate_keys.cpp)。
  • 键顺序不被保留std::map + std::less 的默认组合意味着遍历对象会按字母序而非插入序进行。若需保留插入顺序,可改用 ordered_json / ordered_map(参见 ordered_json 文档docs/mkdocs/docs/examples/ordered_json.cpp)。

版本历史与自定义比较器的注意事项

  • default_object_comparator_t 类型别名自 3.11.0 版本引入;在此之前,比较器类型仅隐含体现在 object_t 的定义中。
  • 相关变更:object_comparator_t3.11.0 起改为条件定义为 typename object_t::key_compare(可用时)或 default_object_comparator_t(否则)。

从源码结构看,比较器透明性还约束了 API 能力边界:当用户为 basic_json 自定义了一个非透明ObjectType/比较器时,依赖 detail::is_transparent<object_comparator_t>::value 的泛型键重载(如接受 string_viewvalue() 重载,见 include/nlohmann/json.hpp)将不再参与重载决议,按键查找会退回到必须提供 object_t::key_type 类型的接口。换句话说,想要继续使用 std::string_view 之类的轻量键类型进行查找,自定义比较器必须满足透明比较器的要求。

小结

default_object_comparator_t 虽只是一个类型别名,却是 JSON 对象键访问行为的关键枢纽:

  1. C++14 之前定义为 std::less<StringType>,C++14 及以后定义为透明比较器 std::less<>,后者使 std::string_view 等非字符串键类型可以直接参与对象查找,避免临时 std::string 的构造;
  2. 实际生效的比较器由 object_t::key_compare 决定,通过 object_comparator_t(源码中的 actual_object_comparator_t)获取;
  3. 比较器是否透明(is_transparent 特征)直接参与 value()at() 等接口重载的 SFINAE 选择,决定了泛型键查找能力是否可用;
  4. 默认比较器与 std::map 的组合带来了字典序存储/序列化、顺序无关的对象相等性等默认行为,这些均可通过 object_t 的类型特化(如 ordered_json)改变。
登录后查看全文
热门项目推荐
相关项目推荐