6

The following code seems to always follow the true branch.

#include <map>
#include <iostream>

class TestClass {
  // implementation 
}

int main() {
  std::map<int, TestClass*> TestMap;
  if (TestMap[203] == nullptr) {
    std::cout << "true";
  } else {
    std::cout << "false";
  }
  return 0;
}

Is it defined behaviour for an uninitialized pointer to point at nullptr, or an artifact of my compiler?

If not, how can I ensure portability of the following code? Currently, I'm using similar logic to return the correct singleton instance for a log file:

#include <string>
#include <map>    

class Log {
  public:
    static Log* get_instance(std::string path);
  protected:
    Log(std::string path) : path(path), log(path) {};
    std::string path;
    std::ostream log;
  private:
    static std::map<std::string, Log*> instances;
};

std::map<std::string, Log*> Log::instances = std::map<std::string, Log*>();

Log* Log::get_instance(std::string path) {
  if (instances[path] == nullptr) {
    instances[path] = new Log(path);
  }
  return instances[path];
}

One solution would be to use something similar to this where you use a special function provide a default value when checking a map. However, my understanding is that this would cause the complexity of the lookup to be O(n) instead of O(1). This isn't too much of an issue in my scenario (there would only ever be a handful of logs), but a better solution would be somehow to force pointers of type Log* to reference nullptr by default thus making the lookup check O(1) and portable at the same time. Is this possible and if so, how would I do it?

Community
  • 1
  • 1

2 Answers2

9

The map always value-initializes its members (in situations where they are not copy-initialized, of course), and value-initialization for builtin types means zero-initialization, therefore it is indeed defined behaviour. This is especially true for the value part of new keys generated when accessing elements with operator[] which didn't exist before calling that.

Note however that an uninizialized pointer is not necessarily a null pointer; indeed, just reading its value already invokes undefined behaviour (and might case a segmentation fault on certain platforms under certain circumstances). The point is that pointers in maps are not uninitialized. So if you write for example

void foo()
{
  TestClass* p;
  // ...
}

p will not be initialized to nullptr.

Note however that you might want to check for presence instead, to avoid accumulating unnecessary entries. You'd check for presence using the find member function:

map<int, TestClass*>::iterator it = TestMap.find(203);
if (it == map.end())
{
  // there's no such element in the map
}
else
{
  TestClass* p = it->second;
  // ...
}
celtschk
  • 19,311
  • 3
  • 39
  • 64
  • Thanks for the quick answer! Just for further clarification, would "TestClass* p();" be initialised to nullptr? –  Sep 22 '12 at 20:08
  • 2
    @MrBushido No. `TestClass* p();` doesn't declare a variable, it declares a function taking no parameters and returning `TestClass*`. – JoeG Sep 22 '12 at 20:09
  • 1
    MrBushido: `int *p;` is default-initialized so has indeterminate value. `int *p{};` is zero-initialized and 0 converted to a pointer is always the null pointer constant so will compare equal to nullptr. – Joseph Mansfield Sep 22 '12 at 20:45
  • 2
    The answer is using the wrong terminology. The `std::map` will *value-initialize* (not *default-initailize*) a new element in `operator[]` if it did not exist. It is important to note the difference, as *default-initalization* of POD types leaves them *uninitialized*, while *value-initialization* means *zero-initialization* for those same types. Replacing *value-initialization* where *default-initialization* appears in the answer will make it right. – David Rodríguez - dribeas Sep 23 '12 at 01:25
  • @DavidRodríguez-dribeas: Did they change that terminology for C++11? Because I'm pretty sure that in the old standard "default-initialization" was defined as zero-initialization for PODs. A quick search [comfirms my memory.](http://stackoverflow.com/a/4524064/1032073) – celtschk Sep 23 '12 at 07:39
  • @celtschk: After rechecking the standard (both C++03 and C++11) it seems they changed the definition of *default-initialize*. What has not changed is that map *value-initializes* in both standards, in the new one it is explicitly said, in the old one through the semantics of `operator[]` being equivalent to `(*((insert(make_pair(x, T()))).first)).second`: `T()` is value initialization of an object of type `T`. – David Rodríguez - dribeas Sep 23 '12 at 12:22
  • @DavidRodríguez-dribeas: I don't think the term "value-initialize" even existed in the old standard, and the old description of "default-initialize" matches the behaviour of `map` (to zero-initialize the value, or to call the default constructor where applicable). Nevertheless I'll change the used term in my post to match the current standard. – celtschk Sep 23 '12 at 12:33
  • 1
    Trust me, I just checked *both* standards. In C++03 the description of `T()` is in 5.2.3p2 *The expression T(), where T is a simple-type-specifier (7.1.5.2) for a non-array complete object type or the (possibly cv-qualified) void type, creates an rvalue of the specified type, which is value-initialized *. The same quote in C++11 reads *The expression T(), where T is a simple-type-specifier or typename-specifier for a non-array complete ob- ject type or the (possibly cv-qualified) void type, creates a prvalue of the specified type,which is value- initialized* – David Rodríguez - dribeas Sep 23 '12 at 13:27
  • @DavidRodríguez-dribeas: OK, I now did a search for the term "value initialized" and found out that it was [introduced in C++03.](http://stackoverflow.com/a/1613578/1032073) I hadn't been aware of that change; my knowledge about those terms came from C++98. – celtschk Sep 23 '12 at 13:34
1

Yes, that's defined behaviour. If an element isn't yet in a map when you access it via operator[], it gets default constructed.

jrok
  • 54,456
  • 9
  • 109
  • 141