https://gcc.gnu.org/bugzilla/show_bug.cgi?id=51971
David Stone <davidfromonline at gmail dot com> changed: What |Removed |Added ---------------------------------------------------------------------------- CC| |davidfromonline at gmail dot com --- Comment #4 from David Stone <davidfromonline at gmail dot com> --- Here are more examples that are currently unclear: ``` struct a { [[gnu::const]] a(): m(1) {} int m; }; ``` If you think of a constructor as a function (that is named after the type) that returns a value of that type, then the default constructor of `a` can safely be marked `gnu::const`. If you think of a constructor as a member function that gets a hidden `this` pointer passed in and then writes into that memory, then the default constructor of `a` cannot even be marked `gnu::pure`. ``` struct b { b(): m(1) {} ~b() {} int m; }; [[gnu::const]] b make_b() { return b(); } ``` Because `b` is not trivially destructible, the Itanium ABI requires that the return value passed by invisible reference. Here, whether it follows the requirements of the attribute depend on the calling convention of your platform if you take the "writing to memory in the return address is a side effect" view. The following function is not even `gnu::pure`, but it is not immediately obvious why and the documentation should probably mention exception handling: ``` int c() { try { throw 1; } catch(int) { return 1; } } ``` The issue here is that `c()` could be called in a destructor, and that destructor could be called during stack unwinding while handling another exception (in which case `std::terminate()` is called rather than this function returning 1). Finally, the documentation is also not clear on what happens if you violate the requirements. Richard Biener's comment suggests that you get some somewhat unspecified result (some unspecified number of your calls might be replaced by the result of another call), and what you are communicating to the compiler is statements about whether side effects matter to you. Another possible interpretation is that the behavior of the program is undefined on violation. These are two very different models for what the attributes mean, but I suspect only the "undefined behavior" model ends up actually being workable in the long run. Consider the following code: ``` int d() { int x; std::vector<int> temporary_buffer; // do some work return x; } ``` >From the perspective of the world, this is logically `gnu::const`, assuming we don't run out of memory. From the "You might get fewer calls and some of your side effects might get discarded" model, I'm perfectly happy if the compiler remembers this result of calling the function and doesn't allocate and deallocate memory multiple times. My code doesn't handle `std::bad_alloc` so I'm fine with it calling `std::terminate` in that rare case, but I'm also fine with it not calling `std::terminate` in cases where I didn't have enough memory, but the result was effectively cached by the compiler. I suspect this will cause strange bugs in the optimizer if users adopt this model: ``` int e() { // After some inlining std::allocator<int>::__some_internal_call(); d(); std::allocator<int>::__some_other_internal_call(); } ``` These internal calls might expect consistent state of the allocator that can be violated if `d` calls `allocate()`. It seems like a safer model to just come right out and say that the behavior of your program is undefined if you violate these requirements.