0

While trying to create a Binary Search Tree (BST) using C++11 I hit a snag. I can't extract a properly created BST out of the function where it is created. The function reads data from a file (in this case just a number on each line) and builds a BST from those. The building part in the loop works correctly. The problem is I can't get the temporary object moved where I want it to be.

Zooming in on the problem I think, see edit 3.

More context: BST<T> is a class that derives publicly from std::unique_ptr<BSTknoop<T>>
BST<T> also inherits the constructors of unique_ptr<BSTknoop<T>> with

template<class T>
using BSTknoopptr=std::unique_ptr<BSTknoop<T>>; // alias template

and

using BSTknoopptr<T>::BSTknoopptr; // in the body of the BST<T> class declaration`

A BSTknoop<T> is a data structure with 3 fields one T object to hold the node data two BST<T> objects left and right (each child is a tree in its own right)

the goal is to move my newly created BST into the calling object. The idea is that in main you can call BST<int> tree; tree.lees(ifs); (with ifs an open input filestream) and tree holds the properly filled BST afterwards.

The function:

template<class T>
istream& BST<T>::lees(istream& is){
    string lijn;
    T knoopwaarde;
    getline(is,lijn);
    knoopwaarde = stoi(lijn);
    BST<T> temptree(new BSTknoop<T>());
    temptree->sl = knoopwaarde;

    for(int i=1; i<DATA_SET_LENGTH ; i++){
        getline(is,lijn);
        knoopwaarde=stoi(lijn);
        temptree.add(knoopwaarde);
        cout<<temptree;
    }
    this->swap(temptree);   /* This does not work */ 
    /* *this = move(temptree);   this does not work either*/
    return is;
}

I have also tried to return a BST<T> from the function and moving that result in the main. That didn't work either.

The program crashes at runtime with an unknown signal.

Side note: I'm unsure as to why BST<T> temptree(new BSTknoop<T>()); works in regard to the templating. The construction works because BST<T> inherits the constructors for unique_ptr<BSTknoop<T>>

edit 1: the declaration of the BST<T> class:

template <class T>
class BST:public BSTknoopptr<T>{
    using BSTknoopptr<T>::BSTknoopptr;
public:
    friend istream& operator>>(istream& is, BST<T>& bb){
        return bb.lees(is);
    }
    friend ostream& operator<<(ostream& os, const BST<T>& bb){
        //return bb.schrijflevelorder(os);
        return bb.schrijfKnoop(os);
    }
    void add(const T&);
    ostream& schrijf(ostream&);
    ostream& schrijfKnoop(ostream&) const;
    int aantalSleutels() const;
    istream& lees(istream&);   
    ostream& schrijflevelorder(ostream& os) const;
private:
};

the declaration of theBSTknoop<T> class:

template <class T>
class BSTknoop{
    friend class BST<T>;
public:
    BSTknoop() {}
    explicit BSTknoop(T _sl) : sl(_sl) {}
private:
    T sl;
    BST<T> links,rechts;
};

edit 2: I've written a move constructor and move assignment operator. I've made sure to retain the default constructor as well using BST<T>()=default; I'm confused though: the BST class doesn't have any members for which I would have to implement my own move constr / operator.
However, BST has inherited from unique_ptr<BSTknoop<T>>so implicitly, it must hold a member of that type. Suppose I want to keep the inheritance, is there any neat way to make this work? Is is also true that I can't (or perhaps shouldn't) implement a copy constr / operator as those are deleted for unique_ptr?

  template<class T>
BST<T>::BST(BST<T>&& other){
    *this = move(other);
}

template<class T>
BST<T>& BST<T>::operator=(BST<T>&& other){
    cout<<"called move operator BST"<<endl;
    (*this).BSTknoopptr<T>::operator=(move(other));
    return *this;
}

edit 3: with my own move constr / operator, the temptree no longer gets filled properly. Below is my code for add which is used to build the tree. If I omit my own move constr / operator, then this works. What I need is an operation that works like this->get()->links = move(tmp). types this->get()->links => BST<T> tmp => BST<T> How come this works? It does the operation I need, why can't I use something similar to do *this = move(temptree) by writing the operation myself.

template<class T>
void BST<T>::add(const T& value){
    if(value <= this->get()->sl){
        if(this->get()->links != nullptr){
            this->get()->links.add(value);
        }
        else {
            BST<T> tmp(new BSTknoop<T>(value));
            this->get()->links = move(tmp);
        }
    }
    else{
        if(this->get()->rechts != nullptr){
            this->get()->rechts.add(value);
        }
        else{
            BST<T> tmp(new BSTknoop<T>(value));
            this->get()->rechts = move(tmp);
        }
    }
}
Str-Gen
  • 33
  • 8
  • 1
    Showing the implementation of your `swap(BST&)` and `operator=(BST&&)` members would be helpful, as well as the definition of the `BST` template class. We can't debug code we can't see. – cdhowie Aug 14 '17 at 20:10
  • 1
    Offhand, I would say that you should probably not inherit from `unique_ptr<>`. Instead, `BST` should *have a* member which is a `unique_ptr<>`. – comingstorm Aug 14 '17 at 20:12
  • 6
    I fear your descent into chaos started when you decided to inherit from `std::unique_ptr`. That way lies madness. – molbdnilo Aug 14 '17 at 20:15
  • 1
    @molbdnilo I think this is exaggerated. This can be fine if done for a specific purpose. Inheriting from `unique_ptr` is the fastest way by far to implement certain other smart pointers. E.g. a smart pointer that simply does a deep copy of the pointee, on copy. – Nir Friedman Aug 14 '17 at 22:00
  • @cdhowie They weren't there before, I hadn't implemented them. I relied on the inherited ones / default generated ones which was probably a mistake. However as it is right now, it still doesn't work. – Str-Gen Aug 14 '17 at 22:41
  • your move constructor/assignment operator don't move. look for double deletion after you tried to move. – Andriy Tylychko Aug 14 '17 at 22:46
  • `.get()` in your move operations should be `.release()`. With `.get()` you are left with two smart pointers that think they own the same object. This will cause use-after-free and double deletion errors. (Or just move the smart pointers.) – cdhowie Aug 14 '17 at 23:05
  • @NirFriedman "slower and correct" is still faster than "faster and incorrect". As `unique_ptr` is not polymorphic I would be really really careful about this. Actually I would strongly advice against it. Emphasis on strongly. – bolov Aug 14 '17 at 23:37
  • @bolov What does polymorphic have to do with anything? Can you show me one way in which this is incorrect? You can advise against it, but meanwhile all the alternatives involve writing out lots of boilerplate. You can have a reasonable implementation say a deep copying pointer in 10 lines of code. – Nir Friedman Aug 15 '17 at 00:36
  • @NirFriedman inheritance is not always the solution. In your case a correct implementation could be done with composition instead. See for instance https://stackoverflow.com/questions/2034916/is-it-okay-to-inherit-implementation-from-stl-containers-rather-than-delegate why inheritance is incorrect. Well, at least very very dangerous. – bolov Aug 15 '17 at 01:05
  • @bolov Please show me how to do a correct implementation without rewriting every member function of `unique_ptr` from scratch. I said from the start that this was the fastest way to do it, and it's fine if used for very specific purposes. It's not a library quality implementation but if I have to write a class that wants a deep copying pointer and I don't have access to a library quality implementation, I would do this. – Nir Friedman Aug 15 '17 at 01:26
  • @NirFriedman if you know, understand and asume the risks, then by all means, do it. But let's be real, it's `unique_ptr`, not `std::string`. How long would it take to write it from scratch? 15 - 20 minutes tops. – bolov Aug 15 '17 at 11:46

1 Answers1

0

Okay, I made the stupidest mistake. There is nothing wrong with the inheritance and accompanying move semantics. I used the DATA_SET_LENGTH preprocessor directive inside my istream& BST<T>::lees(istream& is) function. While my final dataset will hold a million items. My dataset for testing purposes only holds 12. I forgot to change the value of DATA_SET_LENGTH so stoi(lijn) crashed.

For anyone interested in the solution:

notes

  • You don't need to write much code to make this work.
  • It is possible to write BST<T>& operator=(BSTknoopptr<T>&&); yourself and it works. However even without explicitly writing your own it also works.
  • The BST class has no fields of pointer type so we don't get into trouble with unique_ptr not having a virtual destructor

Final question (perhaps someone can comment): The inherited operator operator=(BSTknoopptr<T>&&), what does its signature look like in BST<T>? Mostly in regards to return type (if it's now a member of BST<T> what type does it return?).

** TLDR: why can I rely on the inherited move operator / move constructors of unique_ptr<BSTknoop<T>>? **

bst.h

 #define DATA_SET_LENGTH 12

using namespace std;

template <class T>
class BST;

template <class T>
class BSTknoop;

template<class T>
using BSTknoopptr=std::unique_ptr<BSTknoop<T>>;



template <class T>
class BST:public BSTknoopptr<T>{
    using BSTknoopptr<T>::BSTknoopptr;
public:
    BST<T>& operator=(BST<T>&&) = default;
    BST<T>(BST<T>&&) = default;
    BST<T>()=default;
    //BST<T>& operator=(BSTknoopptr<T>&&);

    friend istream& operator>>(istream& is, BST<T>& bb){
        return bb.lees(is);
    }
    friend ostream& operator<<(ostream& os, const BST<T>& bb){
        //return bb.schrijflevelorder(os);
        return bb.schrijfKnoop(os);
    }
    void add(const T&);
//schrijf schrijft uit in een vorm die min of meer menselijk leesbaar is
    ostream& schrijf(ostream&);
    ostream& schrijfKnoop(ostream&) const;
    int aantalSleutels() const;
    istream& lees(istream&);
//schrijflevelorder schrijft uit in een vorm die door lees(...) kan gelezen worden.
    ostream& schrijflevelorder(ostream& os) const;
private:
};

template <class T>
class BSTknoop{
    friend class BST<T>;
public:
    BSTknoop() {}
    explicit BSTknoop(T _sl) : sl(_sl) {}
private:
    T sl;
    BST<T> links,rechts;
};


//template<class T>
//BST<T>& BST<T>::operator=(BSTknoopptr<T>&& other){
//    cout<<"called BST<T> move with BSTknoopptr r-value ref argument"<<endl;
//    (*this).BSTknoopptr<T>::operator=(move(other));
//    return *this;
//}


template<class T>
void BST<T>::add(const T& value){
    if(value <= this->get()->sl){
        if(this->get()->links != nullptr){
            this->get()->links.add(value);
        }
        else {
            //BSTknoopptr<T> tmp(new BSTknoop<T>(value)); if used with my own BST<T>& BST<T>::operator=(BSTknoopptr<T>&& other)
            BST<T> tmp(new BSTknoop<T>(value));
            this->get()->links = move(tmp);
        }
    }
    else{
        if(this->get()->rechts != nullptr){
            this->get()->rechts.add(value);
        }
        else{
            //BSTknoopptr<T> tmp(new BSTknoop<T>(value)); if used with my own BST<T>& BST<T>::operator=(BSTknoopptr<T>&& other)
            BST<T> tmp(new BSTknoop<T>(value));
            this->get()->rechts = move(tmp);
        }
    }
}

template<class T>
istream& BST<T>::lees(istream& is){
    string lijn;
    T knoopwaarde;
    getline(is,lijn);
    knoopwaarde = stoi(lijn);
    BST<T> temptree(new BSTknoop<T>());
    temptree->sl = knoopwaarde;

    for(int i=1; i<DATA_SET_LENGTH ; i++){
        getline(is,lijn);
        knoopwaarde=stoi(lijn);
        temptree.add(knoopwaarde);
        cout<<temptree;
    }
    *this = move(temptree);
    return is;
}

main.cpp

int main(){
    ifstream ifs;
    ifs.open("c.dat");
    if(ifs.is_open()){
        BST<int> tree;
        tree.lees(ifs);        
        tree.schrijfKnoop(cout);

    }
    else {
        cerr<<"failed to open file"<<endl;
        return -1;
    }
}
Str-Gen
  • 33
  • 8