完整教程:GTSAM中gtsam::NonlinearFactorGraph操作remove(...)、replace(...)和erase(...)使用详解

核心结论总结

  • remove(size_t i)把索引 ishared_ptr 置为 nullptrreset()不改变容器长度(即 size() 不变),留下“空位/洞”。(实现:factors_.at(i).reset();)。
  • replace(size_t index, sharedFactor factor)用新的 shared_ptr 替换索引 index 上的指针at(index) = factor);索引不变,操作是 O(1)。([GTSAM][1])
  • erase(iterator item):调用底层 FastVector::erase(实际是 std::vector 的别名),移除元素并把后面的元素向前移动以填补空位,从而改变被移除后所有因子(及其索引),返回指向“下一个元素”的迭代器(或 end())。erase 的复杂度与被移动元素数线性相关。

逐项详解(含实现 & 语义 & 注意点)

1) void remove(size_t i)

实现(摘自源码)

void FactorGraph<FACTOR>
  ::remove(size_t i) { factors_.at(i).reset();
  }

也即调用了 boost::shared_ptr::reset(),把该槽位变成空指针(nullptr)。

语义 / 效果

  • 容器 factors_ 的长度(size()不改变size() 返回的是底层向量的长度(包含 null 槽)。要统计“活”的因子数量,使用 nrFactors()
  • nrFactors() 会遍历计数非空指针以返回实际存在的因子数量(源码实现遍历 factors_ 并计数 if (factor) ++)。因此 remove 会使 nrFactors() 减 1,但 size() 不变。
  • remove不会移动底层元素,因此不会改变其他因子的索引(索引稳定),也不会使迭代器被重新分配(只要没有其它会改变容器 capacity/size 的操作,现有迭代器仍然有效)。但被 reset() 的单个 shared_ptr 指向的对象会被释放 —— 那个具体对象/引用就失效了。

抛出/异常行为

  • 使用 at(i) 会检查边界;若 i >= size() 会抛 std::out_of_rangeremove 可能抛)。

典型用途

  • 想“逻辑删除”但保留原始索引(例如:外部保存了某些因子的索引,不希望它们因删除而改变)时用 remove
  • 可以和 add_factors(..., useEmptySlots=true) 配合重用这些空槽(源码提供了一个查找空槽并复用的路径)。

示例

gtsam::NonlinearFactorGraph g;
// ... 添加若干因子 ...
g.remove(3);
// 将下标3处置为 nullptr,其他因子索引不变
// g.size() 不变,但 g.nrFactors() 会减少

2) void replace(size_t index, sharedFactor factor)

实现(摘自源码)

void FactorGraph<FACTOR>
  ::replace(size_t index, sharedFactor factor) {
  at(index) = factor;
  }

即直接把 factors_[index] 指向新的 shared_ptr(替换旧指针)。

语义 / 效果

  • 索引稳定:替换不会改变容器大小或其他元素的索引。
  • O(1) 时间。不会移动其他元素,所以不会触发迭代器失效(除非其它线程或外部对容器做了改变)。
  • 如果原来是一个 nullptr 槽,这个操作会“填上”该槽;如果原来是有对象的指针,会先减少旧对象引用计数(可能导致析构)再指向新对象。

边界检查 / 异常

  • at(index) 做边界检查,会在越界时抛异常。

示例

auto f_new = boost::make_shared<MyFactor>
  (...);
  g.replace(5, f_new);
  // 下标5被替换(5 必须 < g.size())

3) iterator erase(iterator item) (和 range 版本)

实现(摘自源码)

iterator FactorGraph<FACTOR>
  ::erase(iterator item) {
  return factors_.erase(item);
  }
  iterator FactorGraph<FACTOR>
    ::erase(iterator first, iterator last) {
    return factors_.erase(first, last);
    }

factors_FastVector,而 FastVector 实际是 std::vector<..., custom_allocator>using 别名;因此 erase 的行为与 std::vector::erase 等价(返回“紧随被删除元素之后”的迭代器)。

语义 / 效果

  • 删除元素并将后续元素整体前移以填补空位 —— 所以 后面所有元素的索引都会减 1。这会影响任何基于索引的外部引用(要特别小心)。
  • 返回值是“下一个有效迭代器”(如果删除的是最后一个元素,则返回 end())。详见 std::vector::erase 语义。([C++参考文献][2])

复杂度与迭代器失效

  • erase 的复杂度与“被移走的元素后面需要移动的元素数量”成线性关系(通常认为是 O(n))。erase使得指向被删除位置及其之后的所有迭代器/引用失效(包括 end())。在删除前面的元素时,后面的迭代器必须被重新获取或使用 erase 的返回值来继续遍历。

正确的迭代删除写法(示例)

for (auto it = g.begin(); it != g.end();
) {
if (shouldRemove(*it)) {
it = g.erase(it);
// erase 返回下一位置,所以用它继续循环
} else ++it;
}

错误示例(会造成未定义行为):

for (auto it = g.begin(); it != g.end();
++it) {
if (cond) g.erase(it);
// 错!erase 使后面的迭代器失效,++it 未定义
}

对比 remove vs erase

  • remove(i):保持索引稳定、在槽位留下 nullptrsize() 不变,nrFactors() 减少。适合保留索引稳定的场景(或延迟压缩)。
  • erase(it):紧缩(compact)容器、改变后续索引、size() 变小。适合你不需要保留原始索引并想减少容器长度的情况。

额外细节、常见坑与建议

  1. size() vs nrFactors() vs empty()

    • size() 返回底层容器长度(包含被 remove() 留下的 nullptr 槽)。nrFactors() 返回实际非空因子数(遍历计数)。empty() 返回底层容器是否为空 —— 如果你用 remove() 把所有槽置为 nullptrempty() 仍然 可能为 false(因为容器还有元素,只是这些元素为 nullptr)。这点常被误用/误解。
  2. 如何复用 remove() 留下的空槽

    • GTSAM 的 add_factors(..., useEmptySlots=true) 路径会尝试找到空槽并复用它们,从而避免容器不断增长。源码里有这段逻辑(查找 !at(i) 的槽并放入新因子)。如果你采用 remove 并希望重用位置,优先使用该接口。
  3. 并发与线程安全

    • 这些操作不是线程安全的。任何修改容器(push_backeraseresize 等)在并发场景下都需要外部同步。
  4. 当外部缓存“因子索引”时

    • 如果你在外部代码保存了某个因子的 index(size_t),并期望之后通过该索引访问该因子:

      • 若你使用 remove(index):索引仍然有效,但 exists(index) 可能变为 false(因为槽为 nullptr)。你需要检查 exists()
      • 若你使用 erase 删除了更前面的元素,则该外部索引会失效(因为索引右移)。避开这个问题的策略:不要保存索引;改用 shared_ptr 或把因子某个稳定的 ID/Key 写进因子里并用查询来查找。
  5. 如何“压缩”掉所有空槽(erase-remove 惯用法)

    • 若你使用 remove 产生了一堆 nullptr,你可以用 std::remove + erase 把它们一次性清理掉(erase-remove 惯用法),例如:
auto new_end = std::remove(g.begin(), g.end(), nullptr);
// 需要 <algorithm>
  g.erase(new_end, g.end());
  // range-erase

这会把所有 nullptr 移到末尾并一次性擦除(更高效)。(标准 erase-remove 惯用法参见 cppreference & 常见资料)。([C++参考文献][4])


小示例(演示三种操作的不同效果)

#include <gtsam/nonlinear/NonlinearFactorGraph.h>
  #include <iostream>
    using namespace gtsam;
    void print_indices(const NonlinearFactorGraph& g) {
    std::cout <<
    "size()=" << g.size() <<
    " nrFactors()=" << g.nrFactors() <<
    "\n";
    for (size_t i=0;i<g.size();
    ++i) {
    std::cout << i <<
    ": " <<
    (g.exists(i) ? "live" : "null") <<
    "\n";
    }
    }
    int main() {
    NonlinearFactorGraph g;
    // 假设 addFactorA/B/C 等都返回 shared_ptr<NonlinearFactor>
      g.push_back(/*factorA*/);
      g.push_back(/*factorB*/);
      g.push_back(/*factorC*/);
      print_indices(g);
      // remove index 1
      g.remove(1);
      std::cout <<
      "after remove(1):\n";
      print_indices(g);
      // replace index 1 (fill it again)
      g.replace(1, /*newFactor*/);
      std::cout <<
      "after replace(1):\n";
      print_indices(g);
      // erase the first factor (index 0)
      auto it = g.begin();
      g.erase(it);
      // now original index 1 的因子会下移到 index 0
      std::cout <<
      "after erase(begin()):\n";
      print_indices(g);
      }

综合示例

下面是一个 完整可编译的小测试程序,用来演示 GTSAM 的 NonlinearFactorGraph

  • remove(size_t i)
  • replace(size_t index, sharedFactor factor)
  • iterator erase(iterator item)

索引nrFactors() / size() 的影响。


测试程序

#include <gtsam/nonlinear/NonlinearFactorGraph.h>
  #include <gtsam/slam/PriorFactor.h>
    #include <gtsam/inference/Symbol.h>
      #include <gtsam/geometry/Pose2.h>
        #include <iostream>
          using namespace std;
          using namespace gtsam;
          int main() {
          NonlinearFactorGraph graph;
          // 添加几个 PriorFactor<Pose2>
            for (int i = 0; i <
            5; i++) {
            Pose2 pose(0.1 * i, 0.2 * i, 0.3 * i);
            auto model = noiseModel::Diagonal::Sigmas(Vector3(0.1, 0.1, 0.1));
            graph.add(make_shared<PriorFactor<Pose2>>
              (Symbol('x', i), pose, model));
              }
              cout <<
              "初始: graph.size()=" << graph.size()
              <<
              " nrFactors()=" << graph.nrFactors() << endl;
              // ---------- remove ----------
              cout <<
              "\n调用 remove(2) ..." << endl;
              graph.remove(2);
              cout <<
              "remove 后: graph.size()=" << graph.size()
              <<
              " nrFactors()=" << graph.nrFactors() << endl;
              for (size_t i = 0; i < graph.size(); i++) {
              cout <<
              " index " << i <<
              ": "
              <<
              (graph[i] ? "valid" : "nullptr") << endl;
              }
              // ---------- replace ----------
              cout <<
              "\n调用 replace(1, newFactor) ..." << endl;
              auto newFactor = make_shared<PriorFactor<Pose2>>
                (
                Symbol('x', 99), Pose2(9, 9, 9),
                noiseModel::Diagonal::Sigmas(Vector3(1, 1, 1)));
                graph.replace(1, newFactor);
                cout <<
                "replace 后: graph.size()=" << graph.size()
                <<
                " nrFactors()=" << graph.nrFactors() << endl;
                for (size_t i = 0; i < graph.size(); i++) {
                cout <<
                " index " << i <<
                ": "
                <<
                (graph[i] ? "valid" : "nullptr") << endl;
                }
                // ---------- erase ----------
                cout <<
                "\n调用 erase(graph.begin()+0) ..." << endl;
                graph.erase(graph.begin());
                cout <<
                "erase 后: graph.size()=" << graph.size()
                <<
                " nrFactors()=" << graph.nrFactors() << endl;
                for (size_t i = 0; i < graph.size(); i++) {
                cout <<
                " index " << i <<
                ": "
                <<
                (graph[i] ? "valid" : "nullptr") << endl;
                }
                return 0;
                }

输出结果

假设初始有 5 个因子,运行结果是:

初始: graph.size()=5 nrFactors()=5
调用 remove(2) ...
remove 后: graph.size()=5 nrFactors()=4
index 0: valid
index 1: valid
index 2: nullptr
index 3: valid
index 4: valid
调用 replace(1, newFactor) ...
replace 后: graph.size()=5 nrFactors()=4
index 0: valid
index 1: valid   <-- 被新因子替换
index 2: nullptr
index 3: valid
index 4: valid
调用 erase(graph.begin()+0) ...
erase 后: graph.size()=4 nrFactors()=3
index 0: valid   <-- 原 index 1 的元素前移
index 1: nullptr
index 2: valid
index 3: valid

总结

  • remove(i)

    • 不会改变 size()(容量不变)。
    • 对应位置变为 nullptrnrFactors() 减少 1。
  • replace(i, f)

    • 不改变 size()
    • 用新因子替换旧因子(若原来是 nullptr,则 nrFactors() 增加 1)。
  • erase(it)

    • 真的删除该元素,后面的因子前移。
    • size()nrFactors() 同时减少 1。

参考

  • FactorGraph.h(实现 remove/replace/erase/size/at/...):源码(包含实现位置)。
  • FactorGraph-inst.hnrFactors()add_factors(..., useEmptySlots) 的实现):源码。
  • NonlinearFactorGraph.hNonlinearFactorGraph 继承自 FactorGraph<NonlinearFactor>,因此上面行为适用于 NonlinearFactorGraph)。
  • FastVector(实际上是 std::vector 的别名,使用自定义 allocator):文档说明。
  • std::vector::erase(语义、迭代器失效、返回值、复杂度):cppreference(权威 C++ 参考)。

posted @ 2025-09-09 16:49  wzzkaifa  阅读(50)  评论(0)    收藏  举报