5

To my knowledge a reference cannot be null, but when I run code like this:

#include <iostream>
#include <string>

void test(int i, const std::string& s = nullptr) {
  std::cout << i << " " << s << std::endl;
}

int main() {
  test(1, "test");
  test(2);
}

the optional parameter s can be null, and the code is built. What's more, when test(2) runs, the program throws exceptions instead of printing some random strings.

When I changed s to some basic type like int, it failed to compile, so I think the magic stays inside the string class, but how?

And what's more, how can I check if s is null or not? if I using if(s==nullptr) or if(s.empty()), it fails to compile.

StoryTeller - Unslander Monica
  • 165,132
  • 21
  • 377
  • 458
  • 1
    Does it compile without the `const`? – goodvibration May 06 '19 at 08:25
  • @goodvibration no, it didn't, "non-const lvalue reference to type 'std::string' (aka 'basic_string') cannot bind to a temporary of type 'nullptr_t'" –  May 06 '19 at 08:26
  • 2
    it is not null reference, it creates `s` string object with nullptr as parameter to constructor `string(const char*)`. – rafix07 May 06 '19 at 08:26
  • @rafix07: Should a constructor be called when a reference variable is declared (and if yes, then why is it so only when that reference variable is `const`)? – goodvibration May 06 '19 at 08:27
  • 2
    Undefined behavior... you're toast now dude! The army of undefined-behavior-fanatic-zombies will downvote your question to the abyss of stack overflow, according to the sacred tradition of undefined-behavior blasphemy regulation. – goodvibration May 06 '19 at 08:31
  • @goodvibration Because only a const l-value reference can be bound to a temporary. Non-const cannot (l-value). – Daniel Langr May 06 '19 at 08:33
  • @goodvibration A reference to const can bind to a temporary (constructed in this case with a `const char*` pointer), a reference to non-const cannot bind to a temporary. – Martin Bonner supports Monica May 06 '19 at 08:33
  • @goodvibration Constructor is not called because a reference is declared. It is called because the reference is bound to a temporary object, which needs to be constructed. – Daniel Langr May 06 '19 at 08:34
  • 1
    @goodvibration - There are good UB questions, and there are bad ones. This just so happens to be a decent one. – StoryTeller - Unslander Monica May 06 '19 at 08:46
  • @StoryTeller: I agree. Also, there are good SO participants and there are bad ones. It just so happens that the majority goes under the second type. – goodvibration May 06 '19 at 09:36
  • This is madness. That's one of the reasons why implicit conversion is dropped in new (to dodge arguments that c++ is modern too), modern languages. – xinaiz May 06 '19 at 14:16

2 Answers2

11

test initialized its argument by using constructor number 5 of std::basic_string<char>:

basic_string( const CharT* s,
              const Allocator& alloc = Allocator() );

Since it needs to materialize a temporary (std::string) to bind to that reference. That is because a reference must be bound to an object of a correct type, which std::nullptr_t isn't. And said constructor has a not null constraint on the pointer being passed. Calling test without an explicit argument leads to undefined behavior.

To be perfectly clear, there is no such thing as a null reference in a well-formed C++ program. A reference must be bound to a valid object. Trying to initialize one with nullptr will only seek out to do a conversion.

Since a std::string is an object with a well-defined "empty" state, a fixed version can simply pass in a default initialized string:

void test(int i, const std::string& s = {}); // Empty string by default.

Once the contract violation is fixed, s.empty() should give meaningful results again.

StoryTeller - Unslander Monica
  • 165,132
  • 21
  • 377
  • 458
  • maybe worth to mention it explicitly, just to be sure: references cannot be `NULL`/`nullptr` – 463035818_is_not_an_ai May 06 '19 at 08:30
  • It might be worth pointing out that the author of `test` *probably* wanted `void test(int i, const std::string& s = "")` or `void test(int i, const std::string& s = std::string{})` – Martin Bonner supports Monica May 06 '19 at 08:35
  • And how to check if the basic_string takes a nullptr? I tried "string.empty()" but still got an exception. –  May 06 '19 at 08:37
  • @MartinBonner - Sure to that too – StoryTeller - Unslander Monica May 06 '19 at 08:37
  • @user463035818 Relevant: [dcl.ref/5](http://eel.is/c++draft/dcl.ref#5) *Note: In particular, a null reference cannot exist in a well-defined program, because the only way to create such a reference would be to bind it to the “object” obtained by indirection through a null pointer, which causes undefined behavior.* – Swordfish May 06 '19 at 08:37
  • @reavenisadesk The constructor will dereference the `nullptr` and most likely crash. – Swordfish May 06 '19 at 08:37
  • @reavenisadesk - You can't use a type's API to check a contract violation. The contract must be adhered to for the API to be guaranteed to even work. Consider stepping through the code, and then consulting the documentation. – StoryTeller - Unslander Monica May 06 '19 at 08:38
  • Oh, does the exception throw from the constructor? As soon as it takes a nullptr as parameter? If this, then it is sure there's no way to check. –  May 06 '19 at 08:39
  • @reavenisadesk That's up to the implementation of your Standard Library. If you want to check whats going on step through the code with a debugger. Microsofts (and I bet every) Standard Library will try to find the length of the passed zero terminated string using `strlen()` which will dereference the `nullptr` and boom you go. – Swordfish May 06 '19 at 08:42
3

Reference indeed can not be null, however const std::string& s = nulltr does not do what you think it does. When second parameter is not specified compiler will create a string object invoking implicit string constructor that takes a pointer to null-terminated string as first parameter. So test(2); invocation looks like this:

test(2, ::std::string(static_cast<char const *>(nullptr), ::std::string::allocator_type()));

Note that passing nullptr as this first parameter causes Undefined Behavior.

user7860670
  • 35,849
  • 4
  • 58
  • 84