Navigate C+ by its code graph, not grep
If you are a model reading or editing a C+ codebase, your first instinct is probably to grep for a name. Resist it. C+ ships a resolved, typed code graph through its compiler, and querying that graph answers the questions you actually have, where text search only guesses.
Why grep is the wrong tool here
Text search is lossy on three questions you ask constantly:
- It cannot tell the
Pointstruct from a local variable namedpoint, so a search forpointreturns both and you cannot tell which is which. - It cannot follow
prefix::Itemto the module that actually defines it. - It cannot answer "who calls this function", because a textual hit does not know whether it is a call, a definition, or a string.
Every one of those is a resolution question, and resolution is exactly what grep throws away. The C+ compiler already computes it on every build. C+ keeps it and makes it queryable instead of discarding it.
The graph is the compiler's own resolution, kept
cpc query answers by symbol and type, not by text. Because the answers are resolved, math::area and a local area are distinguished, and a method call binds to the concrete Type::method it dispatches to.
cpc query def math::area # resolved definition site(s)
cpc query refs Point::translate # every use site
cpc query callers process_frame # who calls it
cpc query callees render # what it calls
cpc query type-at src/main.cplus:42:10 # type of a param/field/local at a position
cpc query members Vec # fields + methods of a type
cpc query symbols src/main.cplus # outline of a file
Every result is JSON with clickable file:line:col locations, the same shape diagnostics emit, so you act on a result without parsing prose.
One call for a function's whole neighborhood
When you are about to edit a function, ask for its context pack in a single call:
cpc query context sum_range
{
"kind": "context",
"target": {
"id": "src.geo::Shape::area",
"kind": "method",
"name": "area",
"location": { "file": "src/geo.cplus", "line": 12, "col": 8 },
"signature": "fn area(self) -> f64",
"is_pub": true
},
"callers": [
{ "id": "src.main::render", "kind": "function", "name": "render",
"location": { "file": "src/main.cplus", "line": 4, "col": 1 }, "is_pub": false }
],
"callees": [
{ "id": "src.geo::Shape::perimeter", "kind": "method", "name": "perimeter",
"location": { "file": "src/geo.cplus", "line": 18, "col": 8 }, "is_pub": true }
],
"type_refs": [
{ "symbol": "src.geo::Shape", "kind": "type",
"location": { "file": "src/geo.cplus", "line": 12, "col": 12 }, "in_context": "src.geo::Shape::area" }
],
"unresolved": 0
}
That one call gives you the signature, who calls it, what it calls, the types it touches, and how many calls inside it the graph could not resolve. That is the edit pack you would otherwise assemble from several grep passes and a guess at which area matched. The symbol IDs use source names (src.geo::Shape::area), never a monomorphized area__Shape, so an ID pastes straight back into source.
In an agent loop, use the resident server
cpc query runs each lookup as a one-shot subprocess. For an agent loop, run cpc mcp instead: it builds the graph once, keeps it warm, and exposes the queries as tools over stdio (newline-delimited JSON-RPC). Point your MCP client at cpc mcp and call the tools directly:
find_definitionfind_referencesfind_callerscode_contexttype_at
The same index backs cpc lsp, so the editor and you share one resolved view of the code. There is no second source of truth to keep in sync.
Know where the graph ends
The graph is honest about its own coverage. Call and reference answers carry an explicit unresolved count and a scope field, so you know exactly where resolution stops and a grep fallback is genuinely needed (for example, a symbol reached only through a raw function pointer). Use that signal: query the graph first, read the unresolved field, and fall back to text search only for what it tells you it could not resolve.
The rule
For C+ navigation, query the graph before you reach for grep. It resolves the names text search cannot, it returns machine-readable locations you can act on without parsing, and it tells you where its own coverage ends. Grep is the fallback, not the default.
For the full reasoning behind why C+ is shaped this way, see C+ for LLMs; for the rest of the command surface, see the tooling reference.
‹ Back to all guides