You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

C++如何动态构建指定被追踪函数的调用关系图?

实现核心思路

用线程局部存储(TLS)维护当前调用栈中最近的已追踪节点指针,配合RAII类型的宏自动处理节点注册、父节点绑定、当前节点栈的进出,不需要手动解析调用栈即可自动获取父节点。


第一步:实现核心工具代码

#include <string>
#include <vector>
#include <mutex>
#include <stack>
#include <iostream>
#include <memory>

// 调用图节点结构
struct CallNode {
    std::string funcName;
    std::vector<std::shared_ptr<CallNode>> children;
    CallNode* parent = nullptr;
    explicit CallNode(std::string name) : funcName(std::move(name)) {}
};

// 单例调用图存储
class CallGraph {
public:
    static CallGraph& instance() {
        static CallGraph inst;
        return inst;
    }

    // 注册新节点,返回节点指针
    CallNode* registerNode(const std::string& funcName, CallNode* parent) {
        std::lock_guard<std::mutex> lock(mtx);
        auto node = std::make_shared<CallNode>(funcName);
        node->parent = parent;
        if (parent) {
            parent->children.push_back(node);
        } else {
            rootNodes.push_back(node);
        }
        return node.get();
    }

    // 按要求格式打印调用图
    void print() const {
        std::lock_guard<std::mutex> lock(mtx);
        for (const auto& root : rootNodes) {
            printNode(root, 0);
        }
    }

private:
    CallGraph() = default;
    mutable std::mutex mtx;
    std::vector<std::shared_ptr<CallNode>> rootNodes;

    void printNode(const std::shared_ptr<CallNode>& node, int depth) const {
        if (depth == 0) {
            std::cout << node->funcName << "\n";
        } else {
            for (int i = 0; i < depth - 1; ++i) std::cout << "    ";
            std::cout << "\\-- " << node->funcName << "\n";
        }
        for (const auto& child : node->children) {
            printNode(child, depth + 1);
        }
    }
};

// 线程局部存储:隔离不同线程的追踪栈,避免调用关系串扰
thread_local std::stack<CallNode*> tls_trackStack;

// RAII追踪守卫:函数进入时注册节点、入栈,函数退出时自动出栈
class TrackGuard {
public:
    // 编译器内置宏自动获取带模板参数的完整函数名,MSVC环境替换为__FUNCSIG__即可
    explicit TrackGuard(const char* funcName) {
        CallNode* parent = tls_trackStack.empty() ? nullptr : tls_trackStack.top();
        m_curNode = CallGraph::instance().registerNode(funcName, parent);
        tls_trackStack.push(m_curNode);
    }

    ~TrackGuard() {
        if (!tls_trackStack.empty() && tls_trackStack.top() == m_curNode) {
            tls_trackStack.pop();
        }
    }
private:
    CallNode* m_curNode = nullptr;
};

// 追踪标记宏:需要追踪的函数第一行加此宏即可
#define TRACK_THIS_FUNC() TrackGuard __track_guard(__PRETTY_FUNCTION__)

第二步:修改业务代码添加追踪标记

只需要在需要追踪的函数第一行加TRACK_THIS_FUNC(),无需修改其他业务逻辑:

template <class TA>
class A
{
public:
    void runTracked()
    {
        TRACK_THIS_FUNC();
        // do stuff
    }
};

template <class TB>
class B
{
public:
    void runNonTracked()
    {
        // 不加宏就不会被追踪
        // do stuff
    }

    void runTracked()
    {
        TRACK_THIS_FUNC();
        // do stuff
    }

    void callATracked()
    {
        TRACK_THIS_FUNC();
        auto a = A<TB>();
        a.runTracked();
        // do stuff
    }
};

void root()
{
    TRACK_THIS_FUNC(); // 自定义根入口加标记就会成为根节点
    auto b1 = B<int>();
    auto b2 = B<double>();
    b1.runTracked();
    b2.runNonTracked();
    b2.callATracked();
    
}

int main()
{
    auto b = B<int>();
    b.runTracked(); // 没有加宏的函数里调用的追踪方法,父节点为空会成为独立根节点
    root();
    // 程序退出前打印调用图
    CallGraph::instance().print();
    return 0;
}

方案特性

  • 自动识别父节点:线程栈自动维护最近的已追踪节点,完全不需要手动解析调用栈
  • 灵活调整追踪范围:加/删TRACK_THIS_FUNC()宏即可调整需追踪的函数
  • 支持模板类/函数:编译器内置宏自动带出模板参数,输出格式完全匹配要求
  • 多线程安全:线程局部存储隔离不同线程的调用栈,加锁保证单例线程安全
  • 无业务侵入:仅添加一行标记,不修改原有业务逻辑

内容的提问来源于stack exchange,提问作者Gábor Pálovics

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.10.08 19:57:02