Why CLion Shows Raw Pointers for Custom C++ Containers (and How to Fix It)
CLion delegates variable rendering to your debugger backend. Learn how to register a GDB pretty-printer so a custom linked list shows logical fields instead of raw pointer addresses.
28 Jun 2026, 23:59 UTC

When the Variables Pane Shows Addresses Instead of Data
You set a breakpoint after populating a custom LinkedList<int>. You expect to see something like size=3 with values 1, 2, 3. Instead, the Variables pane gives you head_ = 0x55555556c2a0 and size_ = 3. To inspect the contents, you either expand a chain of pointer nodes manually or add temporary std::cout statements. The frustration is common, but the fix is not a CLion setting—it is a debugger configuration.
CLion does not implement its own C++ type formatter. It delegates variable rendering entirely to the debugger backend selected in Settings > Build, Execution, Deployment > Toolchains. If the backend does not know how to interpret your type, CLion can only show raw memory. To make custom containers readable, you teach the backend—GDB, LLDB, or NatVis—how to present your type.
The Backend Determines the Formatter Ecosystem
The same project can behave very differently depending on whether you use the bundled GDB, a system GDB, or a system LLDB. Each backend loads a different visualization machinery. GDB relies on Python-based pretty-printers. LLDB uses type summaries and synthetic children providers. On Windows with MSVC, NatVis files provide a separate declarative path. These systems are not interchangeable: a script written for GDB will not automatically work under LLDB.
Before writing any code, confirm which debugger is active. Open the Toolchains settings and note the debugger path. If you switch from bundled GDB to system LLDB later, expect your custom formatter to disappear unless you port it.
Registering a GDB Pretty-Printer for a Custom Container
For GDB, CLion exposes a pretty-printing option that loads the debugger's built-in Python printers for standard library types. Custom printers are registered through a startup script or the .debug_gdb_script section emitted by the compiler. The script runs inside the debugger process and defines two things: a to_string() summary and an optional children() iterator that expands logical elements.
Consider a minimal linked list:
// list.h
#pragma once
#include <cstddef>
template <typename T>
struct Node {
T value;
Node* next;
};
template <typename T>
class LinkedList {
public:
LinkedList() : head_(nullptr), size_(0) {}
void push_back(const T& v);
private:
Node<T>* head_;
std::size_t size_;
};
A GDB pretty-printer for this type might look like the following. Save it as linkedlist_printer.py in your project directory.
import gdb.printing
class LinkedListPrinter:
def __init__(self, val):
self.val = val
def to_string(self):
return "LinkedList(size={})".format(int(self.val['size_']))
def children(self):
node = self.val['head_']
count = int(self.val['size_'])
for i in range(count):
if node == 0:
break
yield "[{}]".format(i), node['value']
node = node['next']
def build_pretty_printer():
pp = gdb.printing.RegexpCollectionPrettyPrinter("project")
pp.add_printer('LinkedList', '^LinkedList<.*>$', LinkedListPrinter)
return pp
gdb.printing.register_pretty_printer(
gdb.current_objfile(),
build_pretty_printer()
)
To load it, reference the script from your GDB initialization. In CLion, locate the debugger configuration for your active toolchain and add the script path to the GDB init file or .gdbinit field. Alternatively, place a .gdbinit in your home directory containing source /absolute/path/to/linkedlist_printer.py. The debugger process needs read access to that file; no other special permissions are required.
Expected check: Set a breakpoint after several push_back calls. The Variables pane should display LinkedList(size=3) with expandable children [0], [1], and [2] instead of a bare Node* pointer. If you see a Python traceback in the debugger console, the script failed to load.
LLDB and NatVis Take a Different Route
If your toolchain uses LLDB, the GDB script above does nothing. LLDB visualizes types through type summaries and synthetic children, typically loaded from an LLDB init file. The syntax and registration mechanism are entirely separate. On Windows/MSVC toolchains, NatVis files provide yet another path: you write declarative XML that the debugger reads to display native types. A project that moves between GDB and LLDB must maintain formatters for both backends.
The Trade-off: Readability Versus Stepping Performance
Pretty-printer scripts execute arbitrary Python or LLDB script code inside the debugger process. Only load scripts you trust. More importantly, synthetic children can make stepping feel sluggish. The children() method above walks the entire list every time execution stops. For a container with thousands of elements, that traversal runs on every breakpoint hit and can introduce noticeable latency.
A safer pattern for large containers is to provide only to_string() and skip children(), or to cap the iterator:
def children(self):
node = self.val['head_']
limit = min(int(self.val['size_']), 100)
for i in range(limit):
if node == 0:
break
yield "[{}]".format(i), node['value']
node = node['next']
Debugger version drift adds another limitation. A script tested against one GDB version may behave differently under another, or under the bundled version shipped with CLion. Rendering problems can appear on one machine and not another, or in CI but not locally.
A Practical Verification Path
Start with a minimal CMake project containing your custom container. Populate it, set a breakpoint, and confirm the Variables pane shows raw pointers. Then add one printer for one type and verify that logical fields appear. Switch the active toolchain between bundled GDB and system LLDB to confirm whether your configuration is backend-specific. Finally, run the session in a clean CLion profile or with default settings to rule out project-level configuration causing the observed rendering. If stepping becomes slow, simplify the synthetic children or remove them entirely.
Pretty-printers will not eliminate every debugging session that needs printf, but they remove the most tedious tax: translating walls of addresses into structures you can reason about while stepping.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.