PAYATHON 2026

nlohmann::json 不建议按值传参,为何 parse 仍按值返回?

数据小许

不矛盾。把已有的 nlohmann::json 左值按值传给函数时,通常会复制它;parse 按值返回新建结果时,可以通过复制消除或移动语义将结果交给调用方,通常不会再复制整棵 JSON 数据树。

不建议按值传参,主要是为了避免不必要的复制。函数仍然可以按值返回 JSON 对象。

按值传参为什么可能昂贵?

考虑一个只读取数据的函数:

#include <nlohmann/json.hpp>

using json = nlohmann::json;

void inspect(json value)
{
    // Read value.
}

void example()
{
    json document = {{"items", {1, 2, 3}}};
    inspect(document);
}

document 是左值。调用 inspect(document) 时,参数 value 需要从它复制构造。对于包含对象、数组和字符串的 JSON,这通常意味着复制其中的数据;嵌套结构越大,复制成本越高。

如果函数只读取数据,可以用 const json& 借用已有对象,避免这次复制:

void inspect(const json& value)
{
    // Read value without copying it.
}

如果函数需要直接修改调用方的对象,则使用 json&。

parse 按值返回为什么不同?

问题中的代码是:

basic_json result;
// Parse into result.
return result;

result 是函数新建的局部对象,返回时主要有两种情况:

  1. 发生 NRVO:编译器可以让 result 直接构造在返回对象的存储位置,省去一次复制或移动。NRVO 指命名返回值优化。
  2. 没有发生 NRVO:这里的局部对象满足隐式移动条件,在支持移动语义的 C++ 环境中,可以调用 basic_json 的移动构造函数。

对于通常使用的 nlohmann::json,移动对象、数组或字符串值时,主要是转移内部存储的所有权,无须逐项复制整个嵌套结构。解析时构建数据、分配内存的成本仍然存在。

C++17 是否保证这里完全没有移动?

不能这样理解。示例中的 return result; 返回的是一个有名字的局部变量,即使在 C++17 中,NRVO 也不是强制要求。

C++17 对某些纯右值场景保证复制消除。例如:

auto document = nlohmann::json::parse("{\"enabled\":true}");

在 C++17 及之后,调用方的这次同类型初始化不会额外要求一次复制或移动;但 parse 内部命名局部变量的返回,仍需区分 NRVO 和未发生 NRVO 时的移动。

返回语句直接写 return result; 即可,不需要改成 return std::move(result);。后者会阻止该返回语句应用 NRVO。

按值参数什么时候合理?

如果函数需要持有一份独立的数据,或者接收调用方交出的数据,按值参数可以是合理设计:

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

using json = nlohmann::json;

class Store {
public:
    explicit Store(json value)
        : value_(std::move(value)) {}

private:
    json value_;
};

不同调用方式的成本不同:

  • Store store(document);:复制已有对象,再移动到成员中;适用于调用方需要保留原数据的情况。
  • Store store(std::move(document));:允许移动已有对象;之后不要依赖 document 仍保留原内容。
  • Store store(json::parse(text));:传入新解析的结果,可以利用复制消除和移动语义。

判断参数类型时,可以先看函数是否需要独立持有数据。

如何解析到已有 JSON 对象?

通常直接赋值就足够:

json document;
document = json::parse(text);

这里先产生解析结果,再通过赋值更新 document。右侧是临时结果,可以使用移动赋值,无须为了赋值再深复制整棵 JSON 树。

对于输入流,也可以使用公开的 operator>>:

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

std::istringstream input("{\"enabled\":true}");
nlohmann::json document;
input >> document;

这种写法可以将解析结果写入已有对象,但不保证复用原有容器容量,也不保证解析过程不创建临时对象。如果目标是减少内存分配,需要结合实际使用的库版本和数据进行测量,不能仅凭接口形式判断。

备注:内容仅供参考。