Search code examples
c++c++11unique-ptrmove-semantics

getting a segfault when I try to deep copy `unique_ptr`s


Why can't I return a unique_ptr from a clone function? I thought I could do this.

I have a base class for different mathematical functions called transform. I have a container of pointers to this type because I am using polymorphism. For example, all these derived classes have a different implementation of log_jacobian that is useful for statistical algorithms.

I am using unique_ptrs to this transform class, and so I have made a (pure virtual) clone function that makes new unique_ptr pointing to a deep copy of the same mathematical transform object. This new object is of the same type that derives from transform<float_t>, but it's a separate because you can't have two unique_ptrs pointing to the same thing.

template<typename float_t>
class transform{
...
virtual std::unique_ptr<transform<float_t>> clone() const = 0;
...
};

My transform_container class holds a few of these at a time. After all, most statistical models have more than one parameter.

template<typename float_t, size_t numelem>
class transform_container{
private:

    using array_ptrs = std::array<std::unique_ptr<transform<float_t>>, numelem>;
    array_ptrs m_ts;
    unsigned m_add_idx;
...
    auto get_transforms() const -> array_ptrs;
};

I'm not sure why the deep copy function get_transforms isn't working, though. It is used to make copies, and to access individual transformations from the container. I get a segfault when I run some tests. If I run it in gdb, it explicitly tells me the line with a comment after it is bad.

template<typename float_t, size_t numelem>
auto transform_container<float_t,numelem>::get_transforms() const -> array_ptrs
{
    array_ptrs deep_cpy;
    for(size_t i = 0; i < numelem; ++i){
        deep_cpy[i] = m_ts[i]->clone(); // this line
    }
    return deep_cpy;
}

I've also tried std::moveing it into deep_cpy[i] and using unique_ptr::reset, but to no avail.

Edit:

here are some other relevant methods: a method that adds transforms to transform_container, and the factory method for the individual transform:

template<typename float_t>
std::unique_ptr<transform<float_t> > transform<float_t>::create(trans_type tt)
{
    if(tt == trans_type::TT_null){
        
        return std::unique_ptr<transform<float_t> >(new null_trans<float_t> );
    
    }else if(tt == trans_type::TT_twice_fisher){
        
        return std::unique_ptr<transform<float_t> >(new twice_fisher_trans<float_t> );
    
    }else if(tt == trans_type::TT_logit){
        
        return std::unique_ptr<transform<float_t> >(new logit_trans<float_t> );
    
    }else if(tt == trans_type::TT_log){

        return std::unique_ptr<transform<float_t> >(new log_trans<float_t> );
    
    }else{

        throw std::invalid_argument("that transform type was not accounted for");
    
    }
}

template<typename float_t, size_t numelem>
void transform_container<float_t, numelem>::add_transform(trans_type tt)
{
    m_ts[m_add_idx] = transform<float_t>::create(tt);
    m_add_idx++;
}


Solution

  • In get_transforms(), you are looping over the entire m_ts[] array, calling clone() on all elements - even ones that have not been assigned by add_transform() yet! Unassigned unique_ptrs will be holding a nullptr pointer, and it is undefined behavior to call a non-static class method through a nullptr.

    The easiest fix is to change the loop in get_transforms() to use m_add_idx instead of numelem:

    template<typename float_t, size_t numelem>
    auto transform_container<float_t,numelem>::get_transforms() const -> array_ptrs
    {
        array_ptrs deep_cpy;
        for(size_t i = 0; i < m_add_idx; ++i){ // <-- here
            deep_cpy[i] = m_ts[i]->clone();
        }
        return deep_cpy;
    }
    

    Otherwise, you would have to manually ignore any nullptr elements, eg:

    template<typename float_t, size_t numelem>
    auto transform_container<float_t,numelem>::get_transforms() const -> array_ptrs
    {
        array_ptrs deep_cpy;
        for(size_t i = 0, j = 0; i < numelem; ++i){
            if (m_ts[i]) {
                deep_cpy[j++] = m_ts[i]->clone();
            }
        }
        return deep_cpy;
    }
    

    Either way, you should update add_transform() to validate that m_add_idx will not exceed numelem:

    template<typename float_t, size_t numelem>
    void transform_container<float_t, numelem>::add_transform(trans_type tt)
    {
        if (m_add_idx >= numelem) throw std::length_error("cant add any more transforms"); // <-- here
        m_ts[m_add_idx] = transform<float_t>::create(tt);
        ++m_add_idx;
    }
    

    That being said, since transform_container can have a variable number of transforms assigned to it, I would suggest changing transform_container to use a std::vector instead of a std::array, eg:

    template<typename float_t>
    class transform_container{
    private:
    
        using vector_ptrs = std::vector<std::unique_ptr<transform<float_t>>>;
        vector_ptrs m_ts;
    ...
        auto get_transforms() const -> vector_ptrs;
    };
    
    template<typename float_t>
    auto transform_container<float_t>::get_transforms() const -> vector_ptrs
    {
        vector_ptrs deep_cpy;
        deep_cpy.reserve(m_ts.size());
        for(const auto &elem : m_ts){
            deep_cpy.push_back(elem->clone());
        }
        return deep_cpy;
    }
    
    template<typename float_t>
    std::unique_ptr<transform<float_t>> transform<float_t>::create(trans_type tt)
    {
        switch (tt) {
            case trans_type::TT_null:
                return std::make_unique<null_trans<float_t>>();
    
            case trans_type::TT_twice_fisher:
                return std::make_unique<twice_fisher_trans<float_t>>();
        
            case trans_type::TT_logit:
                return std::make_unique<logit_trans<float_t>>();
        
            case trans_type::TT_log:
                return std::make_unique<log_trans<float_t>>();
        }    
    
        throw std::invalid_argument("that transform type was not accounted for");
    }
    
    template<typename float_t>
    void transform_container<float_t>::add_transform(trans_type tt)
    {
        m_ts.push_back(transform<float_t>::create(tt));
    }