C++20 Compile Time Evaluation, Optimization and Smartpointers

Viewed 147

Using std::shared_ptr does not allow for compile time evaluation, constexpr, inlining and other optimizations. This is probably due to multithreading safety.

So I experimented with my own simple smart-pointer, just to see what is possible if thread safety is not needed. This is just a minimal draft of an intrusive, linked list reference tracking smart pointer (see the comments in the example for more info).

Questions:

  1. The example below compiles to full compile time evaluation on clang. On gcc it does not. Why?
  2. clang does compile time evaluate everything, if, and only if some null pointer checks are included (see ~myptr()). Why?
  3. These null pointer checks are not actually needed, due to the invariant of the smart pointer. Is there an attribute etc. that tells clang to trust the pointer is never null?

https://godbolt.org/z/1xK1r4YjT

#include <memory>
//#include <iostream>

// Simplistic linked list reference tracking smart pointer.
//
// Linking instead of reference counting is interesting because it allows for extra functionality such as
// moving around referenced memory blocks (growable objects, defragmentation), "set null" 
// semantics on delete, etc.
// 
// Not thread safe, but (trying to) support a high level of compile time evaluation, inlining, 
// optimization. 
template<typename T>
struct myptr {
private:
    T * p;
    myptr * next;
public:
    constexpr myptr(T * p) noexcept : p(p), next(p ? p->first : nullptr) {
        p->first = this;
    }
    constexpr myptr(const myptr & other) noexcept : myptr(other.p) {}

    constexpr T &operator*() {
        return *p;
    }
    constexpr T *operator->() {
        return p;
    }
    T &operator=(const myptr & other) = delete;
    T &operator=(const myptr && other) = delete;

    constexpr ~myptr() noexcept {
        if (p) {
            // We must remove `this` from the list. As these are typically stack references, they will be 
            // mostly LIFO, so we will typically find it immediately.
#if 1
            myptr ** pnext = &(p->first);
            while (*pnext) { 
                if (*pnext == this) {
                    // Found our smart pointer, bypass.
                    *pnext = next;
                    break;
                }
                pnext = &((*pnext)->next);
            }
#else
            // NOTE: the loop with null check (above) should not be necessary, as the invariant of myptr holds 
            // that we will always find `this` in the list and never hit a null pointer. 
            // However, if we use the code below, clang will immediately stop compile time evaluation. 
            // Is there some "function with potential null pointer fault" restriction in play?
            myptr ** pnext = &(p->first);
            while (*pnext != this) { 
                pnext = &((*pnext)->next);
            }
            // Bypass.
            *pnext = next;
#endif
            next = nullptr;
            if (!p->first) {
                // The last reference vanished.
                delete p;
            }
            p = nullptr;
        }
    }
};

struct Obj {
    int i{20};
    inline ~Obj() noexcept {
        i = -1;
        //std::cout << "destructor" << std::endl;
    }
private:
    // intrusive linked references list
    struct myptr<Obj> * first{};
    friend struct myptr<Obj>;
};

// Try different pointers:
using obj_ptr = myptr<Obj>;
//using obj_ptr = std::shared_ptr<Obj>;
//using obj_ptr = Obj*;

//constexpr
inline 
int fun(obj_ptr obj) {
    return obj->i;
}

int main() {
    obj_ptr obj(new Obj);
    {   obj_ptr obj2(obj);
        obj2->i += 1;
    }
    obj_ptr obj3(obj);
    obj3->i *= 2;
    return fun(obj);
}
1 Answers

The example below compiles to full compile time evaluation on clang. On gcc it does not. Why?

Because neither behavior is required. constexpr doesn't mean "function will execute at compile-time". It means that the compiler may do such evaluation at compile-time. It is only required if the function is called in a context that must evaluate to a constant expression.

And that's not what you're doing here.

The optimization you're seeing on Clang has nothing to do with constexpr qualifiers. Take those qualifiers away, and Clang still optimizes it away. They're just regular compiler optimizations.

clang does compile time evaluate everything, if, and only if some null pointer checks are included (see ~myptr()). Why?

Compiler optimizations are picky and sometimes very arbitrary.

Related