Search code examples
c++language-lawyerdefault-constructorc++98default-arguments

Is a single argument constructor with a default value the same as a default constructor?


Specifically in the context of automatic calling of the base class constructor: Is a single argument constructor of a base class with a default value handled the same way (e.g. automatically called if not specified otherwise) as a default constructor (a constructor without arguments)?

struct base {
  base(int value = 42) {}
};
struct derived : public base {
  derived() {} // automatic call to base::base(int) ?
};

edit: The following does NOT relate to the question, it's just how I came upon this. The following code even does not exhibit the crash I've been seeing. See below for an actual example.

Consider this:

#include <sstream>
// C++98, std::ostringstream(ios_base::openmode mode = ios_base::out) available
struct OhNo : public std::ostringstream {
  OhNo() {
  }
  void Crash() const {
    this->str();
  }
};
// later: OhNo f; f.Crash();

std::ostringstream (prior to C++11) didn't have a no-argument constructor. Just one with a single argument and a default value. The above OhNo does not call the constructor of it's base class. (Yes it does) AFAIK the base class constructor is automatically called if there's a no-argument constructor available.

GCC 5.4.0 compiles fine with this but later segfaults (due to the uninitialized base class another issue). Clang 7.0.0 also compiles fine with this and also runs the code without issue.

Who's right? Is it necessary to manually call the base class constructor here? Answer: No!

Affected code: UnitTest++ MemoryOutStream class

Related issue: https://github.com/unittest-cpp/unittest-cpp/issues/174


Ok, I've no idea what's going on. Disassembly of the below shows that the base constructor is called. So the answer is most probably "YES". For anyone who's interested, here's how to reproduce this very strange behaviour:

#include "UnitTest++.h"

volatile double A() { return 2; }

TEST(Crash) {
  CHECK_CLOSE(1,A(),0.1);
}

int main()
{
 int exit_code = 0;
 exit_code = UnitTest::RunAllTests();
 return exit_code;
}

Compile with g++ against libunittest++.a and it's headers (for example from https://packages.ubuntu.com/xenial/amd64/libunittest++-dev/download).

Run it normally:

test.cc:5: error: Failure in Crash: Unhandled exception: Crash!
FAILURE: 1 out of 1 tests failed (1 failures).
Test time: 0.00 seconds.

Run it in gdb:

(gdb) catch throw
Catchpoint 1 (throw)
(gdb) run
Starting program: /home/musteresel/huh/libunittest++-dev_1.4.0-3_amd64/data/usr/lib/a.out 

Program received signal SIGSEGV, Segmentation fault.
0x00007ffff7f205a0 in std::string::assign(std::string const&) () from /nix/store/hlnxw4k6931bachvg5sv0cyaissimswb-gcc-7.4.0-lib/lib/libstdc++.so.6
(gdb) bt
#0  0x00007ffff7f205a0 in std::string::assign(std::string const&) () from /nix/store/hlnxw4k6931bachvg5sv0cyaissimswb-gcc-7.4.0-lib/lib/libstdc++.so.6
#1  0x0000000000403a7a in UnitTest::MemoryOutStream::GetText() const ()
#2  0x0000000000402871 in UnitTest::CheckClose<int, double, double> (results=..., expected=@0x7fffffffbabc: 1, actual=@0x7fffffffbac0: 2, 
    tolerance=@0x7fffffffbac8: 0.10000000000000001, details=...) at ../include/unittest++/Checks.h:53
#3  0x0000000000402483 in TestCrash::RunImpl (this=0x408060 <testCrashInstance>) at test.cc:6
#4  0x0000000000402bc2 in void UnitTest::ExecuteTest<UnitTest::Test>(UnitTest::Test&, UnitTest::TestDetails const&) ()
#5  0x0000000000403255 in UnitTest::TestRunner::RunTest(UnitTest::TestResults*, UnitTest::Test*, int) const ()
#6  0x0000000000403683 in UnitTest::RunAllTests() ()
#7  0x0000000000402514 in main () at test.cc:14
(gdb) 

Disassembly - clearly shows constructor is being called:

# in UnitTest::CheckClose<int, double, double>
  4027cb:       e8 e2 fd ff ff          callq  4025b2 <UnitTest::MemoryOutStream::MemoryOutStream()>

# in UnitTest::MemoryOutStream::MemoryOutStream()
  4025eb:       e8 60 fa ff ff          callq  402050 <std::__cxx11::basic_ostringstream<char, std::char_traits<char>, std::allocator<char> >::basic_ostringstream(std::_Ios_Openmode)@plt>


Solution

  • From ISO/IEC 14882:1998(E) [class.ctor]/5

    A default constructor for a class X is a constructor of class X that can be called without an argument.

    A default constructor may have parameters as long as they all have default arguments and, thus, the constructor can be called without arguments.

    If you do not explicitly specify a mem-initializer for your base class in your constructor's initializer list, then the base class will be initialized via its default constructor.

    std::basic_ostringstream had a default constructor already in C++98 (a constructor with one parameter which had a default argument). If you look closely, you'll find that cppreference page you linked confirms this…