Adrian
Adrian

Reputation: 10911

Why does gcc/clang think that a returned constexpr object is not constexpr?

I've tried this with VC++, gcc and clang and I get slightly different results for all of them.

Here is the relevant code (full code can be seen in the Demo links above):

template <typename Type, size_t Size>
class ArrayStack {
public:
    using iterator               = Type*;
    using const_iterator         = Type const*;
    using value_type             = Type;
    using pointer                = Type*;
    using reference              = Type&;
    using difference_type        = std::make_signed_t<size_t>;
    using size_type              = size_t;
    using reverse_iterator       = std::reverse_iterator<iterator>;
    using const_reverse_iterator = std::reverse_iterator<const_iterator>;

    constexpr ArrayStack()
        : m_stack{}
        , m_end{ begin() }
    {}

    // Init only used part of m_stack is only available in C++20 
    constexpr ArrayStack(ArrayStack const& to_copy)
        : m_stack{}
        , m_end{ begin() }
    {
        *this = to_copy;
    }

    constexpr ArrayStack& operator=(ArrayStack const& rhs) {
        // std::copy is constexpr in C++20
        auto it{ begin() };
        for (auto rhs_it{ rhs.begin() }, rhs_end_it{ rhs.end() };
            rhs_it != rhs_end_it;
            ++it, ++rhs_it)
        {
            *it = *rhs_it;
        }
        m_end = begin() + rhs.size();
        return *this;
    }
//...
    constexpr               iterator   begin()       { return m_stack; }
    constexpr         const_iterator   begin() const { return m_stack; }
//...
    constexpr               iterator     end()       { return m_end; }
    constexpr         const_iterator     end() const { return m_end; }
//...
    constexpr void push_back(value_type const& value) {
        if (!full()) {
            m_end++[0] = value;
        }
        else {
            throw std::out_of_range("Ran out of stack space");
        }
    }
//...
    constexpr size_t size() const {
        return end() - begin();
    }

    constexpr value_type const& back() const {
        if (!empty()) {
            return m_end[-1];
        }
        else {
            throw std::out_of_range("Nothing on stack");
        }
    }
//...
    constexpr bool empty() const {
        return m_end == begin();
    }

    constexpr bool full() const {
        return end() == m_stack + Size;
    }

private:
    Type m_stack[Size];
    iterator m_end;
};

constexpr bool fn() {
    ArrayStack<int, 2> stack;
    stack.push_back(2);
    stack.push_back(1);
    return stack.back() == 1;
}

constexpr auto fn2() {
    ArrayStack<int, 2> stack;
    stack.push_back(2);
    stack.push_back(1);
    return stack;
}

int main() {
    static_assert( sizeof(int) == 4, "Should be 4 bytes in size");
    static_assert( fn(), "Should return true.");
    static_assert( fn2().back() == 1, "Should equal 1.");
    constexpr auto y { fn2() };
    static_assert( y.back() == 1, "Should equal 1.");
    constexpr ArrayStack<int, 2> z { fn2() };
    static_assert( z.back() == 1, "Should equal 1.");
    return 0;
}

This appears to be caused by the pointer in the object. Replacing the pointer with an index to one past the end seems to fix the issue, though the data layout is a bit odd as it seems to have values which shouldn't really contribute to anything as the result should be compile time data (with directives showing) and there is a lot more data than there should be, even at -O3. Also, the compile time data isn't used, so it should also be just thrown away.

gcc (with directives showing, using -O3) left is without static Demo, right is with static Demo: Majority of stuff added when vars are declared static in gcc -O3 ... Majority of stuff added when vars not declared static in gcc -O3

clang had way too many differences to be able to analyze effectively. They appeared to be byte value changes, label changes and possibly ordering changes.

MSVC++ (show directives doesn't seem to change anything, using -O3). Left is without static, right is with static. Stuff added when vars are declared static in MSVC++ -O3

Note that without the directives showing with -O3, the main() was the same for gcc and clang (with directives showing and using -O3) :

main:
        xor     eax, eax
        ret

Question is why is the pointer causing such havoc? Am I correct in assuming that this behaviour is a bug on the gcc/clang's part?

Side question: Why is a static constexpr variable resulting in different code than a constexpr variable? As they are both generate compile time constants, they should result in the same code, shouldn't they?

Note:

In my index version, there is a small bug where m_iend isn't assigned to rhs.m_iend at the end of operator=(). This doesn't affect the code path depicted, but it is an error if the assignment is done outside of the copy constructor.

Upvotes: 2

Views: 688

Answers (1)

HTNW
HTNW

Reputation: 29193

MSVC is pretty clearly in error for accepting your code (maybe it's just trying to be "nice"). Clang's error message is to the point: y would contain a pointer (m_end) to some part of itself. Pointers are not allowed in the results of constant expressions unless they refer to static objects. A constexpr variable must be initialized from a constant expression. So y needs to be static to be constexpr.

[expr.const] (as of C++20, but much the same in C++14)

A constant expression is either a glvalue core constant expression that refers to an entity that is a permitted result of a constant expression (as defined below), or a prvalue core constant expression whose value satisfies the following constraints:

  • if the value is an object of class type, each non-static data member of reference type refers to an entity that is a permitted result of a constant expression,
  • if the value is of pointer type, it contains the address of an object with static storage duration, the address past the end of such an object ([expr.add]), the address of a non-immediate function, or a null pointer value,
  • if the value is of pointer-to-member-function type, it does not designate an immediate function, and
  • if the value is an object of class or array type, each subobject satisfies these constraints for the value.

An entity is a permitted result of a constant expression if it is an object with static storage duration that either is not a temporary object or is a temporary object whose value satisfies the above constraints, or if it is a non-immediate function.

Clang is also giving the rationale for why C++ is defined this way: the value of a pointer is an address to an object. If said pointer is supposed to always have the same value (address) no matter when you evaluate it (which is what constexpr means), the object it is referring to must be static. I'm not sure what you're getting at with the assembly: if y, z are static constexpr then Clang makes main a no-op. What more could you want?

That GCC chokes on the static_asserts (but not on the static constexpr auto y{fn2()};!) does seem to me to be a GCC bug.

Having internal pointers in objects is annoying and error-prone anyway. Use an index.

Upvotes: 1

Related Questions