10

Everyone is quick to point out the "Rule of Zero" in code reviews, peer conversations, and Stack Overflow comments/answers.

I am not a believer. I'd like to be. Usually if enough people agree on something enough for it to get its own label and become an idiom, then it is well thought out....

So, let me explain my reason for not believing:

I cannot count the number of times I have been tasked to debug a crash dump from a production crash. While debugging said dump, I can look up and down the call stack and sometimes I have the need to see "When is this class being constructed?" Secondarily, I might need to see "What is its or its members addresses when it is?" This usually leads to some other place in the code after evidence that more instances are being created than should be, or when addresses or sizes are not what is expected.

This could also apply, when I want to see "When is this class being assigned to?"

As such, I've always implement all 5 methods, trivial or not, in order to provide a place to break in future debug sessions while reproducing the conditions in the dump without having to change the source. Without them, there would be no way (easily) to perform the above debugging session.

If you modify source in order to provide a line of code to break on, your crash dump is no longer usable with the source.

Now, some would say that this situation does not arise often enough for them to add more code, and more potential bugs, in the case of trivial constructors and assignment operators. However, I'd argue that one occasion having to explain to your superiors that you have to make another build, redeploy to production, reproduce the crash, and retrieve another dump, in order to even start debugging the problem, is enough to change one's outlook. That is not an enjoyable experience and it has occurred to me a number of times.

So, please make me a believer!

Tell me how I can accomplish the above described debugging session given a crash dump, when there is no line available to break on, after being a good boy and following the "Rule of Zero".

Namely:

  1. Break any time any instance of a given class is being constructed without any lines of constructor code to break on.

  2. Break any time any instance of a given class is being assigned to without any lines of assignment operator to break on.

I am using Visual Studio, and more specifically Visual Studio 2015.

11
  • 2
    Seems like Scott Meyers tends to agree with you: scottmeyers.blogspot.com/2014/03/…
    – B.J.
    Commented Nov 28, 2017 at 6:07
  • 4
    I've never heard of this "rule of zero" Commented Nov 28, 2017 at 6:51
  • 3
    Typically "rule of zero" does not claim itself to be the universal truth. It is usually stated with the caveat that the more correct statement is "rule of (either five or zero, no in-between)". The rule of no in-between is because the compiler decides whether to generate an implicitly defined special function by looking at whether you explicitly declared something or not, even though a compiler is not smart enough to look at what your code does (inside the body of the special functions implemented by you) and generate something sensible or similar.
    – rwong
    Commented Nov 28, 2017 at 12:05
  • 2
    @ChristopherPisz: "The crash does not occur in the constructor" Then why did you bring up "crash dumps"? Also, if a type is so frequently created that I can't breakpoint on the location of its source, then you're going to get a lot of false positives if you breakpoint on its creation. It'd be much easier to localize the site of the object's creation than to try to debug from where it got constructed. And lastly, you can't breakpoint on a trivial default constructor or aggregate initialization either, so how do you deal with such types? Commented Nov 28, 2017 at 16:18
  • 3
    @ChristopherPisz: So the crash dump really has nothing to do with the situation, except to encourage you not to recompile the code. As for breaking on copy/assignment, I just don't see how that helps. My debugging strategy for "an object has malformed data" is to start at where I know it has malformed data and back up to see where it came from. To monitor member functions of that object to see who's breaking the data. Compiler-generated copy/assignment operators cannot break the data, so why would I break-point on them? Commented Nov 28, 2017 at 16:41

4 Answers 4

16

After much debate, many lunches, posts on various forums, and head scratching, a colleague of mine came up with the following:

In Visual Studio, go to Debug->New Breakpoint->Function Breakpoint and enter the fully qualified name of the compiler generated method you wish to break on. For example Foo::Foo will break in Foo's compiler generated default constructor. We can add Foo::Foo(const Foo &) for its copy constructor, Foo::Foo(Foo &&) for its move constructor, etc. Note that you must also include any namespace. Also note that you can add function names that do not exist. Take note of the opaque red dot in the breakpoints window vs empty circle denoting a point that will not get hit.

After the debugger breaks, we can use the callstack window to identify what it broke on and choose to continue or not. If we wish to inspect then we can open the disassembly window and step (F10) to the point the constructor, copy constructor, assignment, etc is complete. We can look for the "ret" instruction to guide us. When it goes one up the call stack, even in the disassembly window, the name of the method is denoted. We can then switch to Locals and view "this" as normal.

This is, of course, specific to Visual Studio. Other debuggers might have something similar.

4
  • I'm sure most, if not all, debuggers are perfectly capable of placing a breakpoint on an auto-generated default constructor, so the OPs reason for needing this seems entirely unfounded... Commented Nov 30, 2017 at 14:05
  • @Sean what is "this" in your statement? Are you saying the answer explaining how to debug compiler generated methods is unfounded or the question about how to debug compiler generated methods is unfounded? Or maybe something else? Like his desire to write out compiler generated methods which led to the asking of the question in the first place? I don't follow. Commented Nov 30, 2017 at 17:25
  • I mean his idea that he needs to explicitly define all the constructors just to have somewhere for his debugger to break... Commented Dec 1, 2017 at 14:26
  • @Sean well sure, that's why he made the post in the first place, because he had a feeling that was the case, but did not know how. Commented Dec 1, 2017 at 15:18
2

Since crash dumps can only be troubleshot with the original binaries and debug symbols generated from the original source code, any suggestions that ask to change the source code, or to recompile, do not help with the present situation (of crash dumps that OP has already received). These suggestions could only improve the prognosis of troubleshooting the crash dumps of future releases of the software.

(Rant.) Should I mention that the typical cost to technical support to investigate a non-obvious crash dump involving some kind of data trashing and reconstruction of program and execution state is about $1000 USD? If you can spend one week of work to prevent one crash dump in the future, that seems worth it. I see outsourcing opportunity here (to lower-cost countries) but to let a third-party investigate a core dump means you give away the entirety of the company's tangible software secret.


In general, the ability to read and understand the assembly code shown in the Visual Studio step debugger is a requirement for one to work with crash dumps.

The choices are:

  • Acquire the ability to handle this task yourself.
    • Learn to read assembly code and techniques for reconstructing an understanding of the program state and the path of execution prior to the crash when given a crash dump.
  • Let your supervisor know so that the task can be reassigned
  • Delegate this task to someone else

Techniques that I use when faced with same situation.

The first rule before working on a crash dump is to try to reproduce the same crash yourself. If you can recreate the conditions that lead to a crash, it enables you to troubleshoot a live instance of the crashing program (i.e. you can step-execute it), as opposed to a frozen instance (i.e. a crash dump which does not allow you to step-execute).

If a function is known to have been inlined, then I try to locate all of the callers (technically "call sites"), and set breakpoints there. If even the callers are inlined, I will chase their parent callers until I find call sites that aren't inlined.

Note that, for the purpose of locating all possible call sites, you can do that on a rebuild of slightly modified source code. The knowledge of "all possible call sites" is a knowledge you can take away from one version of source code, and most of that knowledge is still applicable on the crash dump you are working on.

To track down a struct, it is necessary to know its address. A typical way in which the address of a struct is revealed in disassembly code is when that address was passed into a function call.


Are there problems with the "rule of zero"?

I agree that it is often misapplied (and would be better if this issue is fixed in the source code for future releases), and shares my own experience.


Personally I do follow the "rule of three" or "rule of five" for most C++ classes if at least one of the constructor / destructor is not trivial default.

I do this for two reasons.

The first reason is similar to yours. When a constructor is not trivial default, that means there is some "code" that the compiler must generate for you; these generated code may need to call some other code, such as the constructors for some members of the class. If those other code were written by you, you will want to be able to debug them. Providing a user-defined constructor makes this task easier.

The second reason is that, when a constructor is implicitly declared, in some situations it can cause issues when I try to store that type into an STL vector. The superficial reason was related to the problem of incomplete types, but since I'm not certain about the underlying reason and it's not related to this question so I will stop here.


However, providing a user-defined constructor does not always turn off inlining for these methods - the compiler may inline some of them e.g. the constructor anyway. To truly prevent inlining, you would use declspec(noinline) with MSVC.

The ability to use disassembly debugging on optimized builds (builds made with "Release" configuration) depends on both inlining and the version of MSVC. Visual C++ 2017 has better handling for maintaining the map between disassembly address and line-of-code location for inlined functions.


Sometimes, the disassembly debugger skipped over an empty constructor, because it is truly empty. A C-compatible "POD" type is typically believed to be initialized with memset(p, 0, sizeof(*p)) or a sequence of XOR RAX, RAX; MOV [...], RAX which are effectively zero-filling instructions. In some cases they aren't zero-filled at all, i.e. there aren't any instructions. The disassembly debugger cannot stop on non-existent disassembly addresses.


Just as a offhand remark, I find that:

  • The destructor of a type appears to generate more "stuff that are visible in disassembly". Most noticeable is if the destructor is called as part of a deletion on dynamic memory (delete). In the disassembly, one will see an actual function call to an address associated with some_type::scalar deleting destructor. Once a module (EXE or DLL) has been loaded into the process space, one can set a breakpoint at its disassembly address.

  • I tend to follow two-phase initialization in my project, due to reasons which aren't pertinent to this question. But because of this project guideline, I find that errors tend to be thrown not from the constructor, but instead from the second function which does the heavy-lifting.

  • I also use exceptions with string messages. If your project doesn't use exceptions, you may consider error logging as an alternative for capturing details about errors.


The key to debugging problems with a struct is to know (record) its address with certainty. There are many techniques. However, a discussion of these techniques may make the discussion too broad.

2
  • 1
    Not trying to be argumentitive.Obviously, you put effort into this answer, but I am having trouble zeroing in on it. Are you suggesting that I follow the rule of zero and overcome the debugging problem by "debugging disassembly?" I am not sure how to "debug disassembly" or find the constructor of a class in order to break on every instance's creation when doing so. Will google, but any links or further information would be appreciated. Commented Nov 28, 2017 at 15:48
  • I tried a debugging simple example if a class using the compiler generated methods with the dissassembler window open. I can see the constructor if I happen to step over it, but see no way to break on all instances of it being called without source to break on. It gives the address, and I can set a break on it in that run, but once I exit the debugger and start again, in order to get all instances of it being called the breakpoint gets disabled. This was using VS2015. Commented Nov 28, 2017 at 16:28
1

The inevitable result of relying on many abstractions is the loss of ability to do certain things. You can put a breakpoint in a virtual function that gets called, but you can't put a breakpoint on the virtual dispatch logic itself. That is, you can't find out when anyone calls a base class's virtual function, since the function that gets called could be any of the derived class overrides.

The same goes here with special member functions. The compiler generated code for you, and there's nowhere for you to put a breakpoint. That's life.

So, you can either choose the advantages of the Rule of Zero (ease of maintenance due to DRY, ease of implementation (have you ever forgotten to check for self-assignment?), etc) or take the debuggability of manually writing the function. Or you can just do both: when you want to put a breakpoint in a special member function, write the special member function then. And then, remove it after you're done debugging.

It should also be noted that the advantages of using compiler-generated functions involve things that you cannot get otherwise. For example, trivial copyability. You cannot write a trivial copy constructor; if you provide a copy constructor implementation, even if it does exactly what the compiler one would, the type is no longer TriviallyCopyable.

And that's not something you should give up lightly.

1
  • 3
    In the real world, you do not want to modify code when debugging a crash after being handed a dump from something in production. You want the exact same source, when attempting to reproduce the crash. To add code after the fact not only invalidates debugging the dump, but it also runs the risk of adding unintentional errors when attempting to reproduce the scenario. Commented Nov 28, 2017 at 16:31
0

In my opinion, the "rule of zero" is not a rule. It's a design pattern. You don't win points for using it and you don't get penalized by the Rule of Zero Police for disobeying it*; but it might make your code easier to read and maintain.


In my opinion, the "rule of zero" does not forbid you to write a constructor that performs some logging function or, a constructor that exists so that you can set a breakpoint on it. A constructor only violates the rule if it performs some explicit initialization of the object's members.


* Or, maybe they do. Consider looking for a different employer if that is the case.

1
  • 1
    If you write a copy constructor for purposes of breaking on it, then you are going to have to perform the trivial initialization, which in that case is copying the members of rhs, no? True, a constructor itself, not a copy constructor, can omit the initialization and rely on the default values. However, if our purpose was to find "any time an instance is created", then we'd break on all constructors and copy constructors. Commented Nov 28, 2017 at 15:53

Not the answer you're looking for? Browse other questions tagged or ask your own question.