Featured image for: Value range propagation in GCC with Project Ranger.

Modern versions of the C++ programming language have a feature known as lambda expressions. This article shows how you can debug lambda expressions using GDB, the GNU Project Debugger. Even if you're not interested in debugging lambdas, the techniques presented here are useful for many other debugging situations.

What is a lambda expression?

A lambda expression provides the C++ programmer with a way to create an anonymous or unnamed function. Lambda expressions are often used in situations where a callback function is desired. Using a lambda expression often makes writing a callback function significantly easier since various pieces of state that are known in the function from which the callback is passed don't need to be packaged up into a data structure which the callback function will later access. This is due to the fact that a C++ lambda expression provides a way to capture in-scope variables which may be later used when the lambda expression is executed.

The example below shows some simple captures in a few of its lambda expressions.

Example program

I'll use the example program below to demonstrate some debugging techniques in addition to showing some of the challenges that might be encountered when debugging lambda expressions. I've included line numbers as comments for some of the lines at which breakpoints might be placed. I've named this program lambda.cc.

#include <stdio.h>
#include <functional>

int successor (int i) { return i + 1; }

int apply0 (int (*fn)(int), int arg)
{
  return fn (arg);
}

int apply1 (std::function<int(int)> fn, int arg)
{
  return fn (arg);
}

std::function<int(int)> make_function(int& x) {
    return [&] (int i) { return i + x; };		/* Line 17 */
}

int main (int argc, char **argv)
{
  int n = 7, m = -28;

  printf ("Answer 1 is %d\n", apply0 (successor, 3));	/* Line 24 */
  printf ("Answer 2 is %d\n", apply1 (successor, 4));	/* Line 25 */

  printf ("Answer 3 is %d\n",
          apply0 ([] (int i) { return i + 1; }, 1));	/* Line 28 */
  printf ("Answer 4 is %d\n",
          apply1 ([] (int i) { return i + 1; }, 2));	/* Line 30 */

  printf ("Answer 5 is %d\n",
          apply1 ([n] (int i) { return i + n; }, 4));	/* Line 33 */

  auto lf2 = make_function (n);				/* Line 35 */
  printf ("Answer 6 is %d\n", apply1 (lf2, 1));		/* Line 36 */

  auto lf3 = make_function (m);
  printf ("Answer 7 is %d\n", apply1 (lf3, -14));	/* Line 39 */
}

Lines 17, 28, 30, and 33 all contain lambda expressions. The lambda expression on line 33 is:

[n] (int i) { return i + n; }

Lambda expressions start with a left square bracket. In this particular lambda expression the left square bracket is followed by n, which is a variable that is being captured for use in the body of the lambda. Note that n is a local variable in the function main. Parentheses enclose a list of formal parameters to the anonymous function being defined; in this case there is just one parameter named i.

Finally, the body of the lambda expression is placed between curly braces, just like a normal function. The body consists of zero or more executable statements. In this case, there is just one executable statement, a return statement. But do note that the expression to return refers to both the captured variable n and the parameter i. Note that there is no function name—that's what makes it anonymous. Lambda expressions are sometimes assigned to variables; this is done indirectly on lines 35 and 38. There are other optional syntactic components as well as many nuances of lambda expressions which are not described here.

Building and debugging the example program

You can use the GNU C++ compiler to compile and link the example program by using this command:

g++ -Wall -g -o lambda lambda.cc

A similar command can be used to build an executable using the LLVM C++ compiler (clang++):

clang++ -Wall -g -o lambda-clang lambda.cc

GDB interactions shown below were performed using GDB 13.1 on a Fedora 37 machine. Except where noted, the example program was compiled using GCC (g++) 12.2.1. When I did use LLVM (clang++) to check compatibility, I used Clang version 15.0.7.

We can begin debugging the example program using GDB as follows:

$ gdb -q lambda
Reading symbols from lambda...
(gdb) start
Temporary breakpoint 1 at 0x401275: file lambda.cc, line 22.
Starting program: /home/kev/examples/lambda 

This GDB supports auto-downloading debuginfo from the following URLs:
  <https://debuginfod.fedoraproject.org/>
Enable debuginfod for this session? (y or [n]) y
Debuginfod has been enabled.
To make this setting permanent, add 'set debuginfod enabled on' to .gdbinit.
[Thread debugging using libthread_db enabled]                                   
Using host libthread_db library "/lib64/libthread_db.so.1".

Temporary breakpoint 1, main (argc=1, argv=0x7fffffffdd88) at lambda.cc:22
22	  int n = 7, m = -28;
(gdb) 

This example shows how GDB is invoked on the executable named lambda. The -q switch causes GDB to not display copyright, warranty and information about how to obtain documentation and help. Once in GDB, the start command is used to run to the first executable line in the main function.

Note, too, that I answered y to enable debuginfod for the session. When debuginfod is enabled, debugging information associated with libraries used by the program may be downloaded for use by GDB.

In the following examples, I won't show (again) the initial step of compiling the program nor the initial steps of debugging the program with GDB. That said, if you want to follow along, I've organized the examples (up to the section Debugging an LLVM compiled program) so that you won't need to restart GDB—you should be able to obtain similar output by simply typing the commands shown here to GDB.

Note: if the version of GDB or version of the compiler used to build the example program differ significantly form the versions noted earlier, it's possible that you'll see output different from what I show here.

Debugging apply0 and successor

Before looking at debugging a lambda expression, let's first look at using GDB to step into the apply0 call at line 24. Note that successor is passed to apply0; the successor function adds one to its int argument and returns this value.  apply0 takes two arguments, the first of which is a pointer to a function with the second being the value to pass to that function.  It simply calls the function with the argument and returns the result. Stepping into apply0 and then successor is straightforward:

(gdb) until 24
main (argc=1, argv=0x7fffffffdd88) at lambda1.cc:24
24	  printf ("Answer 0.0 is %d\n", apply0 (successor, 3));
(gdb) step
apply0 (fn=0x401156 <successor(int)>, arg=3) at lambda1.cc:8
8	  return fn (arg);
(gdb) step
successor (i=3) at lambda1.cc:4
4	int successor (int i) { return i + 1; }
(gdb) print i
$1 = 3
(gdb) backtrace
#0  successor (i=3) at lambda1.cc:4
#1  0x000000000040117f in apply0 (fn=0x401156 <successor(int)>, arg=3) at lambda1.cc:8
#2  0x0000000000401305 in main (argc=1, argv=0x7fffffffdd88) at lambda1.cc:24

The above example shows GDB's until command, which, in this case, can be used to advance program execution to the specified line, in this case line 24. (Note: The until command is used for other purposes too; don't expect it to always advance to the specified line number.)

Next, the step command is used twice, once to step into apply0 and then again into successor.

After that, a print command is used to show the value of the argument i which is in the successor function.

Finally, the backtrace command is used to show the stack trace.

These commands are straightforward and should be unsurprising to anyone accustomed to using GDB. Ideally, we'd like the debugging of lambda expressions to be just as straightforward, though we'll soon see that this is not the case.

Function objects: Debugging apply1 and successor

The first gotcha occurs when using a function object. To demonstrate this, I've defined apply1 to be very similar to apply0, with the only difference being that instead of taking a function pointer as the first argument (as is done in apply0), the first argument of apply1 is a function object which takes an int argument and returns an int. As a reminder, this is what apply1 looks like:

int apply1 (std::function<int(int)> fn, int arg)
{
  return fn (arg);
}

In GDB, we'll first advance to line 25 by first using the tbreak command to place a temporary breakpoint, after which we use the continue command to advance to that breakpoint:

(gdb) tbreak 25
Temporary breakpoint 2 at 0x4012a9: file lambda.cc, line 25.
(gdb) continue
Continuing.
Answer 1 is 4

Temporary breakpoint 2, main (argc=1, argv=0x7fffffffdd98) at lambda.cc:25
25	  printf ("Answer 2 is %d\n", apply1 (successor, 4));	/* Line 25 */

Before attempting to step into apply1, let's create a checkpoint to which we'll come back later:

(gdb) checkpoint
checkpoint 1: fork returned pid 4178814.

Note: GDB's checkpoint command has several limitations: it doesn't work for multi-threaded programs and it also only works on GNU/Linux. But when it can be used, it's very handy for situations where you might wish to return to an earlier program state.

Now, let's see what happens when we try to step into apply1:

(gdb) step
std::function<int (int)>::function<int (&)(int), void>(int (&)(int)) (
    this=0x7fffffffdb90, __f=@0x401156: {int (int)} 0x401156 <successor(int)>)
    at /usr/include/c++/12/bits/std_function.h:437
437		: _Function_base()

What has happened here is that we've stepped into code which is constructing the function object, which is not what we wanted—we wanted to step into apply1. To get past this, we can use the finish command (which returns us to main at line 25) and then step again, which will get us into apply1:

(gdb) finish
Run till exit from #0  std::function<int (int)>::function<int (&)(int), void>(int (&)(int)) (this=0x7fffffffdb90, 
    __f=@0x401156: {int (int)} 0x401156 <successor(int)>)
    at /usr/include/c++/12/bits/std_function.h:437
0x00000000004012bd in main (argc=1, argv=0x7fffffffdd88) at lambda.cc:25
25	  printf ("Answer 2 is %d\n", apply1 (successor, 4));	/* Line 25 */
(gdb) step
apply1(std::function<int (int)>, int) (fn=..., arg=4) at lambda.cc:13
13	  return fn (arg);

A more complicated call might require the GDB user to issue multiple finish / step commands in order to end up in the desired function.

Using GDB's skip command to avoid stepping into function object constructors

Instead of using finish and then step as shown above, let's look at a way that GDB can more directly step into apply1. First, we'll return to the checkpoint that we created earlier—this is done by using the restart command:

(gdb) restart 1
Switching to Thread 0x7ffff7a89400 (LWP 4178814)
#0  main (argc=1, argv=0x7fffffffdd98) at lambda.cc:25
25	  printf ("Answer 2 is %d\n", apply1 (successor, 4));	/* Line 25 */
(gdb)

Now we'll use GDB's skip command.  The skip command shown here will cause GDB to skip, while stepping, any calls to any method in the std::function class, which includes the constructor that we ran into earlier. This particular skip command uses the -rfunction option with the regular expression ^std::function.*. The -rfunction option indicates that the GDB skip machinery should attempt to match functions at which it might stop against the specified regular expression.

In this case, the regular expression is ^std::function.*. The carat (^) matches the beginning of the string being matched, followed by the literal std::function. Finally, the.* matches the rest of the string. (If you want to know more about regular expressions, I wholeheartedly recommend Jeffrey Friedl's book, Mastering Regular Expressions.)

(gdb) skip -rfunction ^std::function.*
Function(s) ^std::function.* will be skipped when stepping.

Now, with the skip in place, let's try the step again:

(gdb) step
apply1(std::function<int (int)>, int) (fn=..., arg=4) at lambda.cc:13
13	  return fn (arg);
(gdb) 

This time, due to the use of the skip command, shown above, we're able to step into apply1 in a similar fashion as shown for apply0.

Attempting to step into the function call in apply1

In attempting to step into the function call on line 13 (shown above), we'll encounter another "gotcha." So let's create another checkpoint:

(gdb) checkpoint
checkpoint 2: fork returned pid 4193641.

Now, let's see what happens when we step:

(gdb) step
14	}

This didn't step into the successor function as expected.  It turns out that the skip command that we used earlier to avoid seeing the call to the function object constructor is now working against us.  When, while stepping, GDB finds a function to skip, it also skips all functions called by the function in question, even if some descendant call is to a function which wouldn't have been otherwise skipped. To better see what's gone wrong, let's restart at the most recent checkpoint and then disable the skip using the skip disable command:

(gdb) restart 2
Switching to Thread 0x7ffff7a89400 (LWP 4193641)
#0  apply1(std::function<int (int)>, int) (fn=..., arg=4) at lambda.cc:13
13	  return fn (arg);
(gdb) info skip
Num   Enb Glob File                 RE Function
1     y      n <none>                y ^std::function.*
(gdb) skip disable 1

Also shown above, is the info skip command, which is used to display a list of the things to be skipped while stepping.

Now, with the skip on methods in std::function disabled, let's try stepping again:

(gdb) step
std::function<int (int)>::operator()(int) const (this=0x7fffffffdb90, 
    __args#0=4) at /usr/include/c++/12/bits/std_function.h:589
589		if (_M_empty())

We've ended up in the implementation of operator()(int) in std::function. While it is possible to end up in successor() after stepping some more, this is tedious. It is better to instead use the break command to set a breakpoint in successor and continue:

(gdb) break successor
Breakpoint 3 at 0x40115d: file lambda.cc, line 4.
(gdb) continue
Continuing.

Breakpoint 3, successor (i=4) at lambda.cc:4
4	int successor (int i) { return i + 1; }
(gdb) print i
$2 = 4
(gdb) backtrace
#0  successor (i=4) at lambda.cc:4
#1  0x00000000004026d8 in std::__invoke_impl<int, int (*&)(int), int> (
    __f=@0x7fffffffdb90: 0x401156 <successor(int)>)
    at /usr/include/c++/12/bits/invoke.h:61
#2  0x00000000004025ac in std::__invoke_r<int, int (*&)(int), int> (
    __fn=@0x7fffffffdb90: 0x401156 <successor(int)>)
    at /usr/include/c++/12/bits/invoke.h:114
#3  0x0000000000402452 in std::_Function_handler<int (int), int (*)(int)>::_M_invoke(std::_Any_data const&, int&&) (__functor=..., __args#0=@0x7fffffffdae4: 4)
    at /usr/include/c++/12/bits/std_function.h:290
#4  0x0000000000402260 in std::function<int (int)>::operator()(int) const (
    this=0x7fffffffdb90, __args#0=4)
    at /usr/include/c++/12/bits/std_function.h:591
#5  0x00000000004011a1 in apply1(std::function<int (int)>, int) (fn=..., arg=4)
    at lambda.cc:13
#6  0x00000000004012d1 in main (argc=1, argv=0x7fffffffdd88) at lambda.cc:25

Note how much more complicated this backtrace is compared to that of apply0 and successor. This is due to the fact that frames corresponding to calls to C++ support functions for invoking a function object are still on the stack.

Stopping in a lambda expression

Thus far, we've encountered difficulties when stepping into a function to which a function object is passed. That problem is easily avoided by either using finish followed by (another) step or by using GDB's skip command.

We've also seen that stepping into a call of a function object is problematic - instead of stepping directly into the function object, we instead step into some C++ implementation details related to making such a function call. Assuming we know the function being called, we can simply set a breakpoint on it and then continue.

This is the approach that should be taken for debugging a C++ lambda expression: set a breakpoint on it. Due to its anonymous nature, you don't know its name, so, instead, you must set the breakpoint via its line number. But this too, might be somewhat surprising. Let's take a closer look by placing a breakpoint on the lambda expression on line 33. As a reminder, the code looks like this:

  printf ("Answer 5 is %d\n",
          apply1 ([n] (int i) { return i + n; }, 4));	/* Line 33 */

This is what happens when we set a breakpoint on line 33:

(gdb) break 33
Breakpoint 4 at 0x40124f: /home/kev/examples/lambda.cc:33. (2 locations)

Note that the message indicates that breakpoint 4 has been set at 2 locations. We'll use the info breakpoints command (abbreviated to info break) to find out more:

(gdb) info break 4
Num     Type           Disp Enb Address            What
4       breakpoint     keep y   <MULTIPLE>         
4.1                         y   0x000000000040124f in operator()(int) const at lambda.cc:33
4.2                         y   0x000000000040136b in main(int, char**) at lambda.cc:33

This shows the two locations upon which the breakpoint has been set. Breakpoint number 4.2 is in main and will be the breakpoint that is hit first. The other breakpoint, indicated by 4.1, is the breakpoint for the lambda expression. Let's see what happens when we continue first to 4.2 and then to 4.1, and then look at some program state.  Note that we'll need to continue twice, stopping once in main, and the second time in the lambda function.

(gdb) continue
Continuing.
Answer 2 is 5
Answer 3 is 2
Answer 4 is 3

Breakpoint 4.2, main (argc=1, argv=0x7fffffffdd98) at lambda.cc:33
33	          apply1 ([n] (int i) { return i + n; }, 4));	/* Line 33 */
(gdb) continue
Continuing.

Breakpoint 4.1, operator() (__closure=0x7fffffffdc00, i=4) at lambda.cc:33
33	          apply1 ([n] (int i) { return i + n; }, 4));	/* Line 33 */
(gdb) print i
$2 = 4
(gdb) print n
$3 = 7
(gdb) backtrace
#0  operator() (__closure=0x7fffffffdc00, i=4) at lambda.cc:33
#1  0x0000000000401ff0 in std::__invoke_impl<int, main(int, char**)::<lambda(int)>&, int>(std::__invoke_other, struct {...} &) (__f=...)
    at /usr/include/c++/12/bits/invoke.h:61
#2  0x0000000000401d3e in std::__invoke_r<int, main(int, char**)::<lambda(int)>&, int>(struct {...} &) (__fn=...) at /usr/include/c++/12/bits/invoke.h:114
#3  0x0000000000401954 in std::_Function_handler<int(int), main(int, char**)::<lambda(int)> >::_M_invoke(const std::_Any_data &, int &&) (__functor=..., 
    __args#0=@0x7fffffffdaf4: 4) at /usr/include/c++/12/bits/std_function.h:290
#4  0x0000000000402260 in std::function<int (int)>::operator()(int) const (
    this=0x7fffffffdc00, __args#0=4)
    at /usr/include/c++/12/bits/std_function.h:591
#5  0x00000000004011a1 in apply1(std::function<int (int)>, int) (fn=..., arg=4)
    at lambda.cc:13
#6  0x0000000000401398 in main (argc=1, argv=0x7fffffffdd98) at lambda.cc:32

Note that there are four stack frames in between apply1 and the lambda function at frame #0. This is the C++ support code responsible for invoking the lambda expression. Also note that GDB is able print both the argument i and the captured variable n which was declared in main.

Stopping in a particular invocation of a lambda expression

The example program defines a function which returns a function object representing a lambda expression:

std::function<int(int)> make_function(int& x) {
    return [&] (int i) { return i + x; };		/* Line 17 */
}

It's used twice as follows:

  auto lf2 = make_function (n);
  printf ("Answer 6 is %d\n", apply1 (lf2, 1));		/* Line 36 */

  auto lf3 = make_function (m);
  printf ("Answer 7 is %d\n", apply1 (lf3, -14));	/* Line 39 */

Suppose that we wish to only stop in the lambda expression invoked on line 39. The lambda expression is actually on line 17, so we'll have to place a breakpoint on that line, but, as we'll see, the code for the lambda expression is also invoked via the call to apply1 on line 36. We'll arrive at the solution in stages.

We can start out by placing a breakpoint on line 17, after first making another checkpoint. (I'm making another checkpoint so that we can easily back up to an earlier execution point without having to start the program from scratch.) Also, we'll take a look at the locations at which GDB has placed breakpoints:

(gdb) checkpoint
checkpoint 3: fork returned pid 2030341.
(gdb) break 17
Breakpoint 5 at 0x4011af: /home/kev/examples/lambda.cc:17. (2 locations)
(gdb) info breakpoint 5
Num     Type           Disp Enb Address            What
5       breakpoint     keep y   <MULTIPLE>         
5.1                         y   0x00000000004011af in operator()(int) const 
                                                   at lambda.cc:17
5.2                         y   0x00000000004011cf in make_function(int&) 
                                                   at lambda.cc:17

The info breakpoint 5 command shows that breakpoint 5.2 is in make_function and that breakpoint 5.1 is in operator()(int), which is the lambda expression. Our goal is to stop only in the lambda expression, so let's disable breakpoint 5.2.

(gdb) disable 5.2
(gdb) info breakpoint 5
Num     Type           Disp Enb Address            What
5       breakpoint     keep y   <MULTIPLE>         
5.1                         y   0x00000000004011af in operator()(int) const 
                                                   at lambda.cc:17
5.2                         n   0x00000000004011cf in make_function(int&) 
                                                   at lambda.cc:17

Now let's see what happens when we continue.  It'll take two continue commands to get to the lambda invoked by the apply1 call at line 39:

(gdb) continue
Continuing.
Answer 5 is 11

Breakpoint 5.1, operator() (__closure=0x7fffffffdc20, i=1) at lambda.cc:17
17	    return [&] (int i) { return i + x; };		/* Line 17 */
(gdb) continue
Continuing.
Answer 6 is 8

Breakpoint 5.1, operator() (__closure=0x7fffffffdc40, i=-14) at lambda.cc:17
17	    return [&] (int i) { return i + x; };		/* Line 17 */
(gdb) backtrace
#0  operator() (__closure=0x7fffffffdc40, i=-14) at lambda.cc:17
#1  0x0000000000401e70 in std::__invoke_impl<int, make_function(int&)::<lambda(int)>&, int>(std::__invoke_other, struct {...} &) (__f=...)
    at /usr/include/c++/12/bits/invoke.h:61
#2  0x0000000000401a79 in std::__invoke_r<int, make_function(int&)::<lambda(int)>&, int>(struct {...} &) (__fn=...) at /usr/include/c++/12/bits/invoke.h:114
#3  0x000000000040174e in std::_Function_handler<int(int), make_function(int&)::<lambda(int)> >::_M_invoke(const std::_Any_data &, int &&) (__functor=..., 
    __args#0=@0x7fffffffdae4: -14)
    at /usr/include/c++/12/bits/std_function.h:290
#4  0x0000000000402260 in std::function<int (int)>::operator()(int) const (
    this=0x7fffffffdc40, __args#0=-14)
    at /usr/include/c++/12/bits/std_function.h:591
#5  0x00000000004011a1 in apply1(std::function<int (int)>, int) (fn=..., 
    arg=-14) at lambda.cc:13
#6  0x0000000000401452 in main (argc=1, argv=0x7fffffffdd88) at lambda.cc:39

The backtrace shows that the second continue command brought us to the lambda expression invoked by the the call to apply1 at line 39 in main. For this simple program, it's not onerous to continue twice, but in real application code, it might happen that hundreds or thousands of continue commands might be needed to stop at the point of interest.

To show how we can stop at a breakpoint which might be hit after some other line of code has been executed, let's first restart from checkpoint 3:

(gdb) restart 3
Switching to Thread 0x7ffff7a89400 (LWP 2030341)
#0  operator() (__closure=0x7fffffffdbf0, i=4) at lambda.cc:33
33	          apply1 ([n] (int i) { return i + n; }, 4));	/* Line 33 */

This causes GDB to switch to the fork created in the previous checkpoint, but it doesn't undo any of the breakpoints that we set up after creating the checkpoint. To see this, let's look again at breakpoint 5:

(gdb) info breakpoint 5
Num     Type           Disp Enb Address            What
5       breakpoint     keep y   <MULTIPLE>         
	breakpoint already hit 2 times
5.1                         y   0x00000000004011af in operator()(int) const 
                                                   at lambda.cc:17
5.2                         n   0x00000000004011cf in make_function(int&) 
                                                   at lambda.cc:17

This is pretty close to the state of breakpoint 5 when we last looked at it. One difference is that it tells us that breakpoint 5 has (overall) been hit 2 times, whereas that message was missing we we looked at it earlier.

Now, just so it's clear where we are in the program, we'll set a temporary breakpoint at line 35 and then continue:

(gdb) tbreak 35
Temporary breakpoint 6 at 0x4013b5: file lambda.cc, line 35.
(gdb) continue
Continuing.
Answer 5 is 11

Temporary breakpoint 6, main (argc=1, argv=0x7fffffffdd88) at lambda.cc:35
35	  auto lf2 = make_function (n);

GDB has stopped on the first call to make_function. The following line, 36, will call apply1 using the function object held in lf2. Recall that it's our goal to stop in the lambda expression invoked by calling apply1 on line 39. In order to realize that goal, let's first disable breakpoint 5 and then look at what info breakpoint says about it:

(gdb) disable 5
(gdb) info break 5
Num     Type           Disp Enb Address            What
5       breakpoint     keep n   <MULTIPLE>         
	breakpoint already hit 2 times
5.1                         y-  0x00000000004011af in operator()(int) const 
                                                   at lambda.cc:17
5.2                         n   0x00000000004011cf in make_function(int&) 
                                                   at lambda.cc:17

Examining the 'Enb' column shows that, overall, breakpoint 5 is disabled, but were it to be enabled, then breakpoint 5.1 would also be enabled, while breakpoint 5.2 would still be disabled. If we were to continue at this point, the program would not stop at either of the breakpoint 5 locations since it is currently disabled.

The next step is to place a breakpoint at line 39 and then add some commands to run when that breakpoint is hit.  Specifically, we'll enable breakpoint 5 as one of the commands. The commands command is used to associate some GDB commands with a breakpoint; the commands command can be given an argument specifying the breakpoint number (to which to associate some GDB commands), but when no breakpoint number is specified, as is shown below, it simply attaches commands to the most recently created breakpoint.

(gdb) break 39
Breakpoint 7 at 0x40142b: file lambda.cc, line 39.
(gdb) commands
Type commands for breakpoint(s) 7, one per line.
End with a line saying just "end".
>enable 5
>continue
>end

After enabling breakpoint 5 (via the command enable 5), a continue command will be issued. Thus, this breakpoint won't stop for user interaction, but will continue execution after first enabling breakpoint #5, which is for the lambda expression that we want to stop in. This is what happens when we continue:

(gdb) continue
Continuing.
Answer 6 is 8

Breakpoint 7, main (argc=1, argv=0x7fffffffdd88) at lambda.cc:39
39	  printf ("Answer 7 is %d\n", apply1 (lf3, -14));	/* Line 39 */

Breakpoint 5.1, operator() (__closure=0x7fffffffdc40, i=-14) at lambda.cc:17
17	    return [&] (int i) { return i + x; };		/* Line 17 */
(gdb) print x
$5 = (int &) @0x7fffffffdb88: -28
(gdb) print i
$6 = -14
(gdb) backtrace
#0  operator() (__closure=0x7fffffffdc40, i=-14) at lambda.cc:17
#1  0x0000000000401e70 in std::__invoke_impl<int, make_function(int&)::<lambda(int)>&, int>(std::__invoke_other, struct {...} &) (__f=...)
    at /usr/include/c++/12/bits/invoke.h:61
#2  0x0000000000401a79 in std::__invoke_r<int, make_function(int&)::<lambda(int)>&, int>(struct {...} &) (__fn=...) at /usr/include/c++/12/bits/invoke.h:114
#3  0x000000000040174e in std::_Function_handler<int(int), make_function(int&)::<lambda(int)> >::_M_invoke(const std::_Any_data &, int &&) (__functor=..., 
    __args#0=@0x7fffffffdae4: -14)
    at /usr/include/c++/12/bits/std_function.h:290
#4  0x0000000000402260 in std::function<int (int)>::operator()(int) const (
    this=0x7fffffffdc40, __args#0=-14)
    at /usr/include/c++/12/bits/std_function.h:591
#5  0x00000000004011a1 in apply1(std::function<int (int)>, int) (fn=..., 
    arg=-14) at lambda.cc:13
#6  0x0000000000401452 in main (argc=1, argv=0x7fffffffdd88) at lambda.cc:39

Note that GDB shows that breakpoint 7 was hit, but it doesn't stop at it. If we wanted to cause that message to not be printed, we could have used the special command silent as the first command in the commands for breakpoint #7. (Note: the silent command can only be used as a command within the commands command.)

The above example also prints the value of the captured variable x and the argument (to the lambda function) i. Frame #6 of the backtrace shows that the call to apply1 was invoked from line 39 in function main. Frames #0, #5, and #6 correspond to lines of code in our example program. The remaining frames, #1 thru #4, show calls to functions within the C++ library.

GDB's ability to execute commands associated with a breakpoint is a powerful feature that's useful in many other situations as well.

Debugging an LLVM-compiled program

As noted earlier, the interactions with GDB shown above were performed against an executable built with GCC 12.1.

If you're using clang++ instead of the GNU compiler, most of the interactions will be similar if not identical to that shown above. One somewhat important difference is the order in which the locations for a breakpoint on a line containing a lambda expression are shown. When using the LLVM compiler, these locations are reversed from that shown for the GNU compiler. E.g., when setting a breakpoint on line 33, we (might) see this instead when debugging an executable produced by the LLVM compiler:

(gdb) break 33
Breakpoint 4 at 0x4013bb: /home/kev/examples/lambda.cc:33. (2 locations)
(gdb) info break 4
Num     Type           Disp Enb Address            What
4       breakpoint     keep y   <MULTIPLE>         
4.1                         y   0x00000000004013bb in main(int, char**) 
                                                   at lambda.cc:33
4.2                         y   0x000000000040205f in main::$_2::operator()(int) const at lambda.cc:33

This isn't especially important for breakpoint 4, but it is important for breakpoint 5 which was placed on line 17. Before, when using the GCC executable, we disabled the breakpoint on 5.2 in order to disable the breakpoint on the outer scope. But, when using an LLVM executable, we instead have to disable the breakpoint on 5.1 instead. I.e.:

(gdb) disable 5.1
(gdb) info breakpoint 5
Num     Type           Disp Enb Address            What
5       breakpoint     keep y   <MULTIPLE>         
5.1                         n   0x00000000004011f7 in make_function(int&) 
                                                   at lambda.cc:17
5.2                         y   0x000000000040195f in make_function(int&)::$_0::operator()(int) const at lambda.cc:17

This could also happen with some past or future version of the GNU compiler. You shouldn't assume that you know the order of the locations, but should instead use the info breakpoints command to figure out which breakpoint location to disable or even delete.

Summary

This article has discussed several problems that might encountered when using GDB to debug function objects. These problems include inadvertently stepping into constructors as well as the related problem of stepping into function object invocation code. In order to avoid the latter problem, it is best to place a breakpoint in the target function. This can be done by name, assuming we know the name, but in the case of lambda expressions, there is no name, so the breakpoint must be placed via line number.

When placing a breakpoint on a line containing a lambda, it's frequently the case that the breakpoint will be placed at multiple locations. The info breakpoints command can be used to determine which location is that of the containing function and which is the lambda; once this is determined, GDB's disable command can be used to cause GDB to not stop at one of those locations.

Finally, a helper breakpoint with associated commands can be used to re-enable a breakpoint that's been disabled. The combination of first disabling a breakpoint on a lambda and then using a helper breakpoint to re-enable the lambda's breakpoint is useful for targeting a specific invocation of a lambda expression or other function object.

Last updated: August 14, 2023