pass shared_ptr to std::bind through std::ref will strip it's polymorphism

112 views Asked by At

My program crashed by SIGSEGV when I try to dynamic_cast a Base class "this" pointer to a Derived class pointer. I am wondering if it has anything to do with std::bind and std::ref. Below is my example code which can produce the problem.

// header h.hpp

#include <functional>
#include <iostream>
#include <map>
#include <memory>
#include <vector>

using namespace std;

template <typename T> 
class Derived;

class Base {
public:
  virtual ~Base() {}

  template <typename T> 
  void foo(const T &t) {
    cout << "Base foo\n";
    auto derived = dynamic_cast<Derived<T> *>(this); // SIGSEGV crash!
    derived->foo(t);
  }

private:
};

template <typename T> 
class Derived : public Base {
public:
  void foo(const T &t) { cout << "Derived foo\n"; }

private:
};

class Outter {
public:
  void init();
  void run();

private:
  void run_foo(shared_ptr<Base> &base);

  class Inner {
  public:
    void run() {
      for (const auto &f : foos_) {
        f();
      }
    }

    void set_foo(function<void()> f) { 
        foos_.push_back(f); 
    }

  private:
    static vector<function<void()>> foos_;
  };

  Inner inner_;
  vector<shared_ptr<Base>> bases_;
};
// source main.cpp

#include "h.hpp"

vector<function<void()>> Outter::Inner::foos_;

void Outter::init() {
  shared_ptr<Base> base = make_shared<Derived<int>>();
  bases_.push_back(base);
  
//   inner_.set_foo(bind(&Outter::run_foo, this, base)); // no ref version
  inner_.set_foo(bind(&Outter::run_foo, this, ref(base))); // ref verison
}

void Outter::run() { inner_.run(); }

void Outter::run_foo(shared_ptr<Base> &base) {
  int t = 123;
  base->foo(t);
}

int main() {
  Outter outter;
  outter.init();
  outter.run();
  return 0;
}

If I commented out the ref version and use the "no-ref" version, the program runs fine and won't break. It seems like the std::ref function is chopping the polymorphism off of the Base shared_ptr, why is that so?

2

There are 2 answers

1
pptaszni On BEST ANSWER

Nothing to do with shared_ptr or polymorphism, your shared_ptr<Base> base simply goes out of scope.

Consider this example:

void foo(std::shared_ptr<int>& ptr)
{
    std::cout << *ptr << std::endl;
}

int main()
{
    std::function<void()> f;
    std::shared_ptr<int> backup;
    {
        std::shared_ptr<int> ptr{new int{7}};
        backup = ptr;
        f = std::bind(foo, std::ref(ptr));
        // f = std::bind(foo, ptr);
    }
    f();
    return 0;
}

If you use std::ref, calling f(); will have undefined behaviour, because ptr is out of scope and foo has a dangling reference, regardless of the fact that the integer it managed is still shared with another shared_ptr still in the scope.

example with addr sanitizer

1
Marek R On

You have basic dangling reference undefined behavior.

Here:

void Outter::init() {
  shared_ptr<Base> base = make_shared<Derived<int>>();
  bases_.push_back(base);
  
//   inner_.set_foo(bind(&Outter::run_foo, this, base)); // no ref version
  inner_.set_foo(bind(&Outter::run_foo, this, ref(base))); // ref verison
}

when this function ends lifetime of local variable base ends. This means that foos_ contains and std::function which uses reference to object which lifetime ended.

Main problem is:

void Outter::run_foo(shared_ptr<Base> &base) 

Why you need here reference to smart a pointer?
This function doesn't do any kind of ownership transfer for its arguments, so proper API for it should use, raw pointer to Base or reference to Base (depending on your coding convention):

void Outter::run_foo(Base* base)

After that there are two ways to fix it:

  • if you can provide warranty on lifetime of object pointed by base, you can just use raw pointer:
inner_.set_foo(bind(&Outter::run_foo, this, base.get()));
  • drop use of std::bind and use lambda:
inner_.set_foo([this, base]() { run_foo(base.get()); });