1

I present to you this code riddle:

Using this compiler:

user@bruh:~/test$ g++ --version

g++ (Ubuntu 7.3.0-16ubuntu3) 7.3.0 Copyright (C) 2017 Free Software Foundation, Inc. This is free software; see the source for copying conditions. There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

...and this compile string:

g++ main.cpp class.cpp -o main -g

...and these files:

class.hpp:

class base {

  public:

    base();

    virtual void f() = 0;
};

class derived : public base {

  public:

    derived( unsigned int id );

    void f() override;

    unsigned int id; 
};

class.cpp:

#include <iostream>

#include "class.hpp"

base::base() {

  return;
}

derived::derived( unsigned int id )
  :
  id( id ),
  base() {

  return;
}

void derived::f() {

  std::cout << "Ahoy, Captain! I am " << id << std::endl;

  return;
}

main.cpp:

#include <iostream>
#include <functional>
#include <vector>

#include "class.hpp"

int main() {

  unsigned int n_elements;

  std::cout << "enter the number of elements: ";

  std::cin >> n_elements;

  std::cout << std::endl;

  std::vector< class derived > v;

  std::vector< std::reference_wrapper< class base > > base_vector_0;

  std::vector< std::reference_wrapper< class base > > base_vector_1;

  for( unsigned int i = 0; i < n_elements; i++ ) {

    v.emplace_back( i );

    base_vector_0.emplace_back( v[ i ] );
  }

  for( unsigned int i = 0; i < n_elements; i++ ) {

    base_vector_1.emplace_back( v[ i ] );
  }

  std::cout << "sanity check:" << std::endl;

  for( unsigned int i = 0; i < n_elements; i++ ) {

    class base &base = v[ i ];

    base.f();
  }

  std::cout << "case 1:" << std::endl;

  for( unsigned int i = 0; i < n_elements; i++ ) {

    base_vector_1[ i ].get().f();
  }

  std::cout << "case 0:" << std::endl;

  for( unsigned int i = 0; i < n_elements; i++ ) {

    base_vector_0[ i ].get().f();
  }

  return 0;
}

...I get the following output:

user@bruh:~/test$ ./main
enter the number of elements: 1

sanity check:
Ahoy, Captain! I am 0
case 1:
Ahoy, Captain! I am 0
case 0:
Ahoy, Captain! I am 0
harrynh3@bruh:~/test$ ./main
enter the number of elements: 2

sanity check:
Ahoy, Captain! I am 0
Ahoy, Captain! I am 1
case 1:
Ahoy, Captain! I am 0
Ahoy, Captain! I am 1
case 0:
Segmentation fault (core dumped)

My questions:

  1. Why does this not segfault when the user supplied argument = 1?

  2. Why does this segfault when the user supplied argument > 1?

My short explanation of what the code does:

Creates many objects derived from an abstract base class. Stores references to the objects in containers as std::reference_wrapper around abstract base class reference. Creates the containers of std::reference_wrapper slightly differently. Calls the derived override of pure virtual function via the std::reference_wrappers. Segfaults specifically in the case denoted in the source code above.

I implore the C++ experts... Please help me! This is fascinating and I have no idea why it is happening! I've probably done something dumb.

Deduplicator
  • 44,692
  • 7
  • 66
  • 118
MK-Ultra-
  • 11
  • 2

2 Answers2

1

You create dangling reference in this snippet of code:

  for( unsigned int i = 0; i < n_elements; i++ ) {

    v.emplace_back( i ); // [1]

    base_vector_0.emplace_back( v[ i ] ); // [2]
  }

[1] add new item, [2] store reference to this item. If vector was rebuit while calling emplace_back, all references are invalidated and you refer to item which doesn't exist. vector is rebuilt when its current capacity is exceed by adding new items into it.

If you want to store exactly n_elements in v vector and avoid rebuilding vector you can call reserve:

  std::vector< class derived > v;
  v.reserve(n_elements); // added

  std::vector< std::reference_wrapper< class base > > base_vector_0;

  std::vector< std::reference_wrapper< class base > > base_vector_1;

  for( unsigned int i = 0; i < n_elements; i++ ) {

    v.emplace_back( i );

    base_vector_0.emplace_back( v[ i ] );
  }

now, when emplace_back is called no references are invalidated, and accessing these references by base_vector_0 is safe.

rafix07
  • 20,001
  • 3
  • 20
  • 33
  • From https://en.cppreference.com/w/cpp/container/vector/emplace_back: If the new size() is greater than capacity() then all iterators and references (including the past-the-end iterator) are invalidated. Otherwise only the past-the-end iterator is invalidated. <-- I overlooked that very important clause. Thank you! – MK-Ultra- Mar 01 '19 at 18:44
0

In fact, your code does this: it stores a reference to temporary in the vector (encapsulated in a reference_wrapper); that reference immediately becomes dangling, as soon as control exits loop body, right after pushing it; next, you retrieve this dangling reference and invoke a virtual member function on it, thus forcing your program to behave undefinedly; and once you ignite an UB it's like a singularity point where all the rational explanations stop.

BTW, control will eventually exit a function once reaching its body end even if you don't put return in there. 0_o

bipll
  • 11,747
  • 1
  • 18
  • 32