4
\$\begingroup\$

I'm trying to write my own shared_ptr/weak_ptr implementation in C++. I have the following requirements:

I do NOT need support for the following:

  • multithreading (synchronisation)
  • support for polymorphic types as the templated type of the shared_ptr (such as shared_ptr Base*)

Reasons for wanting to write my own implementation:

  • need to supply a separate allocator for the control block
  • need to reduce the size of the control block (the standard version has a very large control block due to its support for multithreading and polymorphism among other things)

Concerns:

  • I'm worried about using my current implementation in production code (need some suggestions on how best to thoroughly test it)
  • I'm concerned I may have left out important features from my implementation

The following is what I've written so far (compilable in a C++11 compliant compiler, with main function and example):

#include <iostream>
#include <memory>

struct shared_ptr_control_base
{
    virtual ~shared_ptr_control_base() { }

    void decrement_count_shared() noexcept { m_shared--; }
    void increment_count_shared() noexcept { m_shared++; }

    void decrement_count_weak() noexcept { m_weak--; }
    void increment_count_weak() noexcept { m_weak++; }

    virtual void                     destroy_shared(void*)       noexcept = 0;
    virtual void                     destruct()                  noexcept = 0;
    virtual shared_ptr_control_base* create()              const          = 0;

    uint32_t m_shared = 1;
    uint32_t m_weak   = 0;
};

template <typename SharedType, typename AllocatorType> struct shared_ptr_control_derived : shared_ptr_control_base
{
    shared_ptr_control_derived() = delete;
    shared_ptr_control_derived(AllocatorType a_allocator) : m_allocator(a_allocator) { }

    shared_ptr_control_derived<SharedType, AllocatorType>* create() const
    {
        auto l_alloctor = std::allocator<shared_ptr_control_derived<SharedType, AllocatorType>>();
        auto l_p = l_alloctor.allocate(1);
        l_alloctor.construct(l_p, *this);
        return l_p;
    }

    void destroy_shared(void* a_pointer) noexcept
    {
        m_allocator.destroy(static_cast<SharedType*>(a_pointer));
        m_allocator.deallocate(static_cast<SharedType*>(a_pointer), 1);
    }

    void destruct() noexcept
    {
        auto l_alloctor = std::allocator<shared_ptr_control_derived<SharedType, AllocatorType>>();
        l_alloctor.destroy(this);
        l_alloctor.deallocate(this, 1);
    }

    mutable AllocatorType m_allocator;
};

template <typename SharedType> struct shared_ptr;

template <typename SharedType> struct weak_ptr
{
    friend struct shared_ptr<SharedType>;

    weak_ptr() : m_pointer(nullptr), m_control(nullptr) { }

    weak_ptr(const weak_ptr<SharedType>& a_that) :
        m_pointer(a_that.m_pointer),
        m_control(a_that.m_control)
    {
        std::cout << "weak_ptr<T>::weak_ptr(const weak_ptr<T>&)" << std::endl;

        if (m_control != nullptr)
        {
            m_control->increment_count_weak();
        }
    }

    weak_ptr(weak_ptr<SharedType>&& a_that) :
        m_pointer(a_that.m_pointer),
        m_control(a_that.m_control)
    {
        std::cout << "weak_ptr<T>::weak_ptr(shared_ptr<T>&&)" << std::endl;

        a_that.m_pointer = nullptr;
        a_that.m_control = nullptr;
    }

    weak_ptr(const shared_ptr<SharedType>& a_that) :
        m_pointer(a_that.m_pointer),
        m_control(a_that.m_control)
    {
        std::cout << "weak_ptr<T>::weak_ptr(const shared_ptr<T>&)" << std::endl;

        if (m_control != nullptr)
        {
            m_control->increment_count_weak();
        }
    }

    weak_ptr<SharedType>& operator=(const weak_ptr<SharedType>& a_rhs)
    {
        std::cout << "weak_ptr<T>& weak_ptr<T>::operator = (const weak_ptr<T>&)" << std::endl;

        if (a_rhs.m_control != m_control)
        {
            if (m_control != nullptr) { decrement_destruct(); }

            m_pointer = a_rhs.m_pointer;
            m_control = a_rhs.m_control;

            if (m_control != nullptr) { m_control->increment_count_weak(); }
        }

        return *this;
    }

    weak_ptr<SharedType>& operator=(weak_ptr<SharedType>&& a_rhs)
    {
        std::cout << "weak_ptr<T>& weak_ptr<T>::operator = (weak_ptr<T>&&)" << std::endl;

        if (a_rhs.m_control != m_control)
        {
            if (m_control != nullptr) { decrement_destruct(); }
        }

        m_pointer = a_rhs.m_pointer;
        m_control = a_rhs.m_control;
        a_rhs.m_pointer = nullptr;
        a_rhs.m_control = nullptr;

        return *this;
    }

    weak_ptr<SharedType>& operator=(const shared_ptr<SharedType>& a_rhs)
    {
        std::cout << "weak_ptr<T>& weak_ptr<T>::operator = (const shared_ptr<T>&)" << std::endl;

        if (a_rhs.m_control != m_control)
        {
            if (m_control != nullptr) { decrement_destruct(); }

            m_pointer = a_rhs.m_pointer;
            m_control = a_rhs.m_control;

            if (m_control != nullptr) { m_control->increment_count_weak(); }
        }

        return *this;
    }

    ~weak_ptr()
    {
        if (m_control) { decrement_destruct(); }
    }

    void decrement_destruct()
    {
        m_control->decrement_count_weak();

        if (m_control->m_weak == 0)
        {
            if (m_control->m_shared == 0)
            {
                std::cout << "weak_ptr -> destructing control block" << std::endl;

                m_control->destruct();
            }
        }
    }

    SharedType* operator->() const noexcept { return m_pointer; }

    SharedType& operator*() const noexcept { return *m_pointer; }

    explicit operator bool() const noexcept { return m_control ? m_control->m_shared : false; }

    uint32_t use_count() const noexcept { return m_control ? m_control->m_shared : 0; }

    SharedType* get() const noexcept { return m_pointer; };

private:
    SharedType* m_pointer;
    shared_ptr_control_base* m_control;
};

template <typename SharedType> struct shared_ptr
{
    friend struct weak_ptr<SharedType>;

    shared_ptr() : m_pointer(nullptr), m_control(nullptr) { }

    explicit shared_ptr(SharedType* const a_pointer) :
        m_pointer(a_pointer),
        m_control(nullptr)
    {
        std::cout << "shared_ptr<T>::shared_ptr(T*)" << std::endl;

        if (m_pointer != nullptr) { create_control(std::allocator<SharedType>()); }
    }

    template <typename AllocatorType> explicit shared_ptr(SharedType* const a_pointer, const AllocatorType a_allocator) :
        m_pointer(a_pointer),
        m_control(nullptr)
    {
        if (m_pointer != nullptr) { create_control(a_allocator); }
    }

    shared_ptr(const shared_ptr<SharedType>& a_that) :
        m_pointer(a_that.m_pointer),
        m_control(a_that.m_control)
    {
        std::cout << "shared_ptr<T>::shared_ptr(const shared_ptr<T>&)" << std::endl;

        if (m_control != nullptr)
        {
            m_control->increment_count_shared();
        }
    }

    shared_ptr<SharedType>& operator=(const shared_ptr<SharedType>& a_rhs)
    {
        std::cout << "shared_ptr<T>& shared_ptr<T>::operator = (const shared_ptr<T>&)" << std::endl;

        if (a_rhs.m_control != m_control)
        {
            if (m_control != nullptr) { decrement_destruct(); }

            m_pointer = a_rhs.m_pointer;
            m_control = a_rhs.m_control;

            if (m_control != nullptr) { m_control->increment_count_shared(); }
        }

        return *this;
    }

    ~shared_ptr()
    {
        if (m_control) { decrement_destruct(); }
    }

    SharedType* operator->() const noexcept { return m_pointer; }

    SharedType& operator*() const noexcept { return *m_pointer; }

    explicit operator bool() const noexcept { return m_pointer != nullptr; }

    uint32_t use_count() const noexcept { return m_control ? m_control->m_shared : 0; }

    void decrement_destruct()
    {
        m_control->decrement_count_shared();

        if (m_control->m_shared == 0)
        {
            std::cout << "shared_ptr -> destructing shared object" << std::endl;

            m_control->destroy_shared(m_pointer);

            if (m_control->m_weak == 0) { std::cout << "shared_ptr -> destructing control block" << std::endl; m_control->destruct(); }
        }
    }

    void reset() noexcept { shared_ptr<SharedType>().swap(*this); }

    void reset(SharedType* const a_pointer) noexcept { shared_ptr<SharedType>(a_pointer).swap(*this); }

    void swap(shared_ptr<SharedType>& a_that) noexcept { std::swap(m_pointer, a_that.m_pointer); std::swap(m_control, a_that.m_control); }

    template <typename AllocatorType> void create_control(AllocatorType a_allocator)
    {
        m_control = shared_ptr_control_derived<SharedType, AllocatorType>(a_allocator).create();
    }

    SharedType* get() const noexcept { return m_pointer; };

private:
    SharedType* m_pointer;
    shared_ptr_control_base* m_control;
};

int* allocate(const int a_argument)
{
    std::allocator<int> l_a;
    auto l_p = l_a.allocate(1);
    l_a.construct(l_p, a_argument);
    return l_p;
}

int main()
{
    // don't use this yet, as it might crash the program
    weak_ptr<int> weak_1;

    {
        // allocate memory for an int, and take shared ownership of the memory in the shared_ptr
        shared_ptr<int> shared_1(allocate(42));

        // set the weak_ptr to refer to the memory in the shared_ptr;
        weak_1 = shared_1;

        if (weak_1)
        {
            std::cout << "weak_1 is safe to use" << std::endl;

            *(weak_1.get()) = 47;
            *weak_1 = 42;
            std::cout << *weak_1 << std::endl;
        }
    }

    // shared_1 went out of scope, so was destroyed
    std::cout << "weak_1 control block's shared count: " << weak_1.use_count() << std::endl;

    if (!weak_1)
    {
        std::cout << "weak_1 is NOT safe to use" << std::endl;
    }
}
\$\endgroup\$
  • \$\begingroup\$ I don't think that smart pointer is right replacement for an allocator. Though writing memory allocator is usually much more error prone (e.g. buffer overruns), whereas smart pointer can only leak memory. It might be arguable which one is worse. \$\endgroup\$ – Incomputable Feb 22 '17 at 1:33
  • \$\begingroup\$ I haven't posted about my allocator yet! For now, I'm just using the std::allocator in this example, for both the control block, and the shared memory; this is fine. Have you discovered anything wrong with the code? \$\endgroup\$ – Truncheon Feb 22 '17 at 1:35
  • \$\begingroup\$ I'm just off of the 5 hour coding marathon. I don't think it is a good idea to listen to my ideas right now :) If I'll notice something I'll post a review. By the way, I think that the reason for adding the mandated multithreading is that std::unique_ptr<> should be enough for sequential code, given that ownership semantics are well thought out. \$\endgroup\$ – Incomputable Feb 22 '17 at 1:37
  • 1
    \$\begingroup\$ Welcome to Code Review! I have rolled back the last edit. Please see what you may and may not do after receiving answers. \$\endgroup\$ – Vogel612 Feb 24 '17 at 23:06
4
\$\begingroup\$

Big thing you have to watch are exceptions that will cause you to leak.

    auto l_p = l_alloctor.allocate(1);
    l_alloctor.construct(l_p, *this);   // What happens if this throws?
    return l_p;

    // Need to make sure you re-clain the memory

    auto l_p = l_alloctor.allocate(1);
    try {
        l_alloctor.construct(l_p, *this);
    }
    catch(..) {
        l_alloctor.deallocate(l_p);
        throw;
    }
    return l_p;

The std::allocator call construct() does not need two arguments.

    l_alloctor.construct(l_p, *this);

    // This results in a call to:
    new (lp) SharedType(std::forward<shared_ptr_control_derived<T,A>>(*this));

This means the type you are sharing must have a constructor that takes an allocator as a parameter. You don't need to do this (since C++11). You only need to pass the pointer (which I think is what you actually want).

     l_alloctor.construct(l_p); // See: http://en.cppreference.com/w/cpp/memory/allocator/construct

Why are you creating allocators on each function call?

    auto l_alloctor = std::allocator<shared_ptr_control_derived<SharedType, AllocatorType>>();

This works for simple allocators that use the standard C++ heap. But some allocators keep state (pool allocators). You must use the same allocator object to allocate/create/delete/de-allocate. And you already have a local allocator object stored locally m_allocator so use that.

Your create() and destruct() usage are not going to work well because they don't necessarily use the same allocator object (as the allocator is copied). SO you need to re-work all the use scenarios for this. I would personally make these two methods static.

\$\endgroup\$
0
\$\begingroup\$

I've made some changes to my code based on suggestions by Loki Astari:

  • Added support for moving allocators into the shared_ptr object by use of a make_shared function
  • Added support for exception safety with try/catch blocks (in the make_shared function)
  • Removed code for instantiating allocators each time a function is called (while I don't use stateful allocators myself, it would be nice to support them if possible)

#include <iostream>
#include <memory>

struct shared_ptr_control_base
{
    virtual ~shared_ptr_control_base() { }

    void decrement_count_shared() noexcept { m_shared--; }
    void increment_count_shared() noexcept { m_shared++; }

    void decrement_count_weak() noexcept { m_weak--; }
    void increment_count_weak() noexcept { m_weak++; }

    virtual void destroy_shared(void*) noexcept = 0;
    virtual void destruct()            noexcept = 0;

    uint32_t m_shared = 1;
    uint32_t m_weak   = 0;
};

template <typename AllocatorTypeControl, typename AllocatorTypeShared, typename SharedType>
struct shared_ptr_control_derived : shared_ptr_control_base
{
    shared_ptr_control_derived() = delete;
    shared_ptr_control_derived(const shared_ptr_control_derived<AllocatorTypeControl, AllocatorTypeShared, SharedType>&) = delete;

    shared_ptr_control_derived(shared_ptr_control_derived<AllocatorTypeControl, AllocatorTypeShared, SharedType>&& a_that) :
        m_allocatorControl(std::move(a_that.m_allocatorControl)),
        m_allocatorShared(std::move(a_that.m_allocatorShared))
    {

    }

    shared_ptr_control_derived<AllocatorTypeControl, AllocatorTypeShared, SharedType>&
        operator = (const shared_ptr_control_derived<AllocatorTypeControl, AllocatorTypeShared, SharedType>&) = delete;
    shared_ptr_control_derived<AllocatorTypeControl, AllocatorTypeShared, SharedType>&
        operator = (shared_ptr_control_derived<AllocatorTypeControl, AllocatorTypeShared, SharedType>&&) = delete;

    shared_ptr_control_derived(AllocatorTypeControl&& a_allocatorC,
                               AllocatorTypeShared&& a_allocatorS) :
        m_allocatorControl(a_allocatorC),
        m_allocatorShared(a_allocatorS)
    {

    }

    void destroy_shared(void* a_pointer) noexcept
    {
        m_allocatorShared.destroy(static_cast<SharedType*>(a_pointer));
        m_allocatorShared.deallocate(static_cast<SharedType*>(a_pointer), 1);
    }

    void destruct() noexcept
    {
        decltype(m_allocatorControl) l_temp = std::move(m_allocatorControl);
        l_temp.m_allocator.destroy(this);
        l_temp.m_allocator.deallocate(this, 1);
    }

    mutable AllocatorTypeControl m_allocatorControl;
    mutable AllocatorTypeShared  m_allocatorShared;
};

template <template <typename t> class T, typename AllocatorTypeShared, typename SharedType>
struct Wrangler
{
    T<shared_ptr_control_derived<Wrangler, AllocatorTypeShared, SharedType>> m_allocator;
};

template <typename SharedType> struct shared_ptr;

template <typename SharedType> struct weak_ptr
{
    friend struct shared_ptr<SharedType>;

    weak_ptr() : m_pointer(nullptr), m_control(nullptr) { }

    weak_ptr(const weak_ptr<SharedType>& a_that) :
        m_pointer(a_that.m_pointer),
        m_control(a_that.m_control)
    {
        std::cout << "weak_ptr<T>::weak_ptr(const weak_ptr<T>&)" << std::endl;

        if (m_control != nullptr)
        {
            m_control->increment_count_weak();
        }
    }

    weak_ptr(weak_ptr<SharedType>&& a_that) :
        m_pointer(a_that.m_pointer),
        m_control(a_that.m_control)
    {
        std::cout << "weak_ptr<T>::weak_ptr(shared_ptr<T>&&)" << std::endl;

        a_that.m_pointer = nullptr;
        a_that.m_control = nullptr;
    }

    weak_ptr(const shared_ptr<SharedType>& a_that) :
        m_pointer(a_that.m_pointer),
        m_control(a_that.m_control)
    {
        std::cout << "weak_ptr<T>::weak_ptr(const shared_ptr<T>&)" << std::endl;

        if (m_control != nullptr)
        {
            m_control->increment_count_weak();
        }
    }

    weak_ptr<SharedType>& operator=(const weak_ptr<SharedType>& a_rhs)
    {
        std::cout << "weak_ptr<T>& weak_ptr<T>::operator = (const weak_ptr<T>&)" << std::endl;

        if (a_rhs.m_control != m_control)
        {
            if (m_control != nullptr) { decrement_destruct(); }

            m_pointer = a_rhs.m_pointer;
            m_control = a_rhs.m_control;

            if (m_control != nullptr) { m_control->increment_count_weak(); }
        }

        return *this;
    }

    weak_ptr<SharedType>& operator=(weak_ptr<SharedType>&& a_rhs)
    {
        std::cout << "weak_ptr<T>& weak_ptr<T>::operator = (weak_ptr<T>&&)" << std::endl;

        if (a_rhs.m_control != m_control)
        {
            if (m_control != nullptr) { decrement_destruct(); }
        }

        m_pointer = a_rhs.m_pointer;
        m_control = a_rhs.m_control;
        a_rhs.m_pointer = nullptr;
        a_rhs.m_control = nullptr;

        return *this;
    }

    weak_ptr<SharedType>& operator=(const shared_ptr<SharedType>& a_rhs)
    {
        std::cout << "weak_ptr<T>& weak_ptr<T>::operator = (const shared_ptr<T>&)" << std::endl;

        if (a_rhs.m_control != m_control)
        {
            if (m_control != nullptr) { decrement_destruct(); }

            m_pointer = a_rhs.m_pointer;
            m_control = a_rhs.m_control;

            if (m_control != nullptr) { m_control->increment_count_weak(); }
        }

        return *this;
    }

    ~weak_ptr()
    {
        if (m_control) { decrement_destruct(); }
    }

    void decrement_destruct()
    {
        m_control->decrement_count_weak();

        if (m_control->m_weak == 0)
        {
            if (m_control->m_shared == 0)
            {
                std::cout << "weak_ptr -> destructing control block" << std::endl;

                m_control->destruct();
            }
        }
    }

    SharedType* operator->() const noexcept { return m_pointer; }

    SharedType& operator*() const noexcept { return *m_pointer; }

    explicit operator bool() const noexcept { return m_control ? m_control->m_shared : false; }

    uint32_t use_count() const noexcept { return m_control ? m_control->m_shared : 0; }

    SharedType* get() const noexcept { return m_pointer; };

private:
    SharedType* m_pointer;
    shared_ptr_control_base* m_control;
};

template <typename SharedType> struct friend_struct;

template <typename SharedType> struct shared_ptr
{
    friend friend_struct<SharedType>;
    friend struct weak_ptr<SharedType>;

    shared_ptr() : m_pointer(nullptr), m_control(nullptr) { }

    shared_ptr(const shared_ptr<SharedType>& a_that) :
        m_pointer(a_that.m_pointer),
        m_control(a_that.m_control)
    {
        std::cout << "shared_ptr<T>::shared_ptr(const shared_ptr<T>&)" << std::endl;

        if (m_control != nullptr)
        {
            m_control->increment_count_shared();
        }
    }

    shared_ptr<SharedType>& operator=(const shared_ptr<SharedType>& a_rhs)
    {
        std::cout << "shared_ptr<T>& shared_ptr<T>::operator = (const shared_ptr<T>&)" << std::endl;

        if (a_rhs.m_control != m_control)
        {
            if (m_control != nullptr) { decrement_destruct(); }

            m_pointer = a_rhs.m_pointer;
            m_control = a_rhs.m_control;

            if (m_control != nullptr) { m_control->increment_count_shared(); }
        }

        return *this;
    }

    ~shared_ptr()
    {
        if (m_control) { decrement_destruct(); }
    }

    SharedType* operator->() const noexcept { return m_pointer; }

    SharedType& operator*() const noexcept { return *m_pointer; }

    explicit operator bool() const noexcept { return m_pointer != nullptr; }

    uint32_t use_count() const noexcept { return m_control ? m_control->m_shared : 0; }

    void decrement_destruct()
    {
        m_control->decrement_count_shared();

        if (m_control->m_shared == 0)
        {
            std::cout << "shared_ptr -> destructing shared object" << std::endl;

            m_control->destroy_shared(m_pointer);

            if (m_control->m_weak == 0) { std::cout << "shared_ptr -> destructing control block" << std::endl; m_control->destruct(); }
        }
    }

    void reset() noexcept { shared_ptr<SharedType>().swap(*this); }

    void reset(SharedType* const a_pointer) noexcept { shared_ptr<SharedType>(a_pointer).swap(*this); }

    void swap(shared_ptr<SharedType>& a_that) noexcept { std::swap(m_pointer, a_that.m_pointer); std::swap(m_control, a_that.m_control); }

    SharedType* get() const noexcept { return m_pointer; };

private:
    SharedType* m_pointer;
    shared_ptr_control_base* m_control;
};

template <typename SharedType>
struct friend_struct
{
    template <typename T>
    shared_ptr_control_base*& get_ref_pointer_control(T& a_r_shared_ptr) { return a_r_shared_ptr.m_control; }

    template <typename T>
    SharedType*& get_ref_pointer_shared(T& a_r_shared_ptr) { return a_r_shared_ptr.m_pointer; }
};

template <typename SharedType,
          template <typename t> class AllocatorControl = std::allocator,
          template <typename t> class AllocatorShared = std::allocator,
          typename... Args>

shared_ptr<SharedType> make_shared(Args&&... args)
{
    using AllocatorControlCreate = Wrangler<AllocatorControl, AllocatorShared<SharedType>, SharedType>;

    shared_ptr_control_derived<AllocatorControlCreate, AllocatorShared<SharedType>, SharedType>
        l_d(std::move(AllocatorControlCreate()),
            std::move(AllocatorShared<SharedType>()));

    auto* l_pC = l_d.m_allocatorControl.m_allocator.allocate(1);
    SharedType* l_pS = nullptr;

    try
    {
        l_pS = l_d.m_allocatorShared.allocate(1);
    }
    catch (...)
    {
        l_d.m_allocatorControl.m_allocator.deallocate(l_pC, 1); throw;
    }

    try
    {
        l_d.m_allocatorControl.m_allocator.construct(l_pC, std::move(l_d));
    }
    catch (...)
    {
        l_d.m_allocatorControl.m_allocator.deallocate(l_pC, 1); l_d.m_allocatorShared.deallocate(l_pS, 1); throw;
    }

    try
    {
        l_pC->m_allocatorShared.construct(l_pS, SharedType(std::forward<Args>(args)...));
    }
    catch (...)
    {
        l_pC->m_allocatorControl.m_allocator.destroy(l_pC);
        l_pC->m_allocatorControl.m_allocator.deallocate(l_pC, 1); l_pC->m_allocatorShared.deallocate(l_pS, 1); throw;
    }

    shared_ptr<SharedType> l_s;
    friend_struct<SharedType>().get_ref_pointer_control(l_s) = l_pC;
    friend_struct<SharedType>().get_ref_pointer_shared(l_s) = l_pS;
    return l_s;
}

int main()
{
    auto shared_1 = make_shared<int>(47);
    auto shared_2 = shared_1;

    {
        auto shared_3 = shared_1;

        std::cout << shared_1.use_count() << std::endl;
    }

    shared_2.reset();

    weak_ptr<int> weak_1(shared_1);

    std::cout << weak_1.use_count() << std::endl;

    shared_1.reset();

    std::cout << weak_1.use_count() << std::endl;

    if (!weak_1)
    {
        std::cout << "weak_1 is no longer safe to use" << std::endl;
    }
}
\$\endgroup\$
  • 1
    \$\begingroup\$ I recommend to post a follow-up question, instead of posting answer. There was a meta post about, but I'm typing from phone, so can't find the link. It is pretty straightforward: cut-paste this answer into new question as content of the question and leave a link to old question. \$\endgroup\$ – Incomputable Feb 26 '17 at 0:58
  • 1
    \$\begingroup\$ Even if this is the improved code, so long as your answer doesn't add anything new, it's not really suited as an answer to your question, since it doesn't actually review the code.... for more information, see our meta: meta.codereview.stackexchange.com/q/1463/9357 \$\endgroup\$ – Vogel612 Feb 26 '17 at 1:46

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.