0

I have a class (struct) that contains a static const std::array private member. I want this member to be static and constant (non-writable). It looks like as if adding the initialization through static function, breaks the constness of the member array. I would have expected the compiler to complain when I try to write to the array defined as const. Instead, when I run I get:

  1. SEGFAULT when using list initializer,
  2. Ability to write when using said function

Class also provides iterator by just returning std::array iterator. When I initialize std::array through initializer list (outside of struct declaration), then try to modify the element in the array through iterator, I get SEGFAULT, although compiler doesn't complain (somewhat expected and fine). However, when I initialize the array through another function (in the code below static std::array<int, 4> HalfCircleStatic_init(); , I am able to change the element value, which is not OK. Worth mentioning the code below was just to reproduce the issue. I really need the ability to initialize the static const array of greater size in a non-trivial way - i.e. using some trigonometric functions. I can't understand why is this happening. Any help or direction appreciated. Thanks.

I tried this:

#include <iostream>
#include <array>

struct ArrayContainer
{
    using iterator = typename std::array<int, 4>::iterator ;

    inline constexpr iterator begin() { return iterator(&arr[0]); }
    inline constexpr iterator end()   { return iterator(&arr[0] + arr.size()); }

private:
    static const std::array<int, 4> arr;

    static std::array<int, 4> HalfCircleStatic_init();
};

const std::array<int, 4> ArrayContainer::arr = {1,2,3,4};

std::array<int, 4> ArrayContainer::HalfCircleStatic_init() {
    std::array<int, 4> retVal{};
    for (int i = 0; i < 4; ++i) retVal[i] = i+1;
    return retVal;
}

int main() {
    ArrayContainer arrCont;
    auto it = arrCont.begin();
    std::cout << "Value at 0: " << *it << std::endl;
    *it = 5;
    std::cout << "Value at 0: " << *it << std::endl;
    return 0;
}

Produces:

Value at 0: 1

Process finished with exit code 139 (interrupted by signal 11: SIGSEGV)

If I change initializer (definition) to something like:

const std::array<int, 4> ArrayContainer::arr = ArrayContainer::HalfCircleStatic_init();

I get this:

Value at 0: 1
Value at 0: 5

Process finished with exit code 0

Using GCC 8.4 on Ubuntu

  • 1
    [UB](https://stackoverflow.com/questions/2766731/what-exactly-do-ib-and-ub-mean) can manifest itself as working code. It doesn't mean that the code is actually correct. – Evg Apr 22 '23 at 02:30
  • I understand I probably am invoking a UB somewhere, but what exactly is a UB here? – G Stepanovic Apr 22 '23 at 18:30
  • *but what exactly is a UB here* - An attempt to modify a constant variable. You take a pointer-to-const (`&arr[0]`), cast away constness (`iterator(&arr[0])`) and then use the resulting pointer to write into a const variable (`*it = 5`). Try to replace `iterator(&arr[0])` with `iterator{&arr[0]}`. – Evg Apr 22 '23 at 21:32

3 Answers3

1
> inline constexpr iterator begin() { return iterator(&arr[0]); } inline
> constexpr iterator end()   { return iterator(&arr[0] + arr.size()); }

The above might be an indicator, it should have been possible to return arr.begin(), and arr.end()

Try const_iterator instead of iterator (or make the array member non-const -- if you want to be able to modify it after construction)

  • Even `begin(arr)` and `end(arr)` are available. The golden rule is **do not try pointer arithmetic at home**. For safety, I would derive type from value: `using iterator = decltype(begin(arr));` – Red.Wave Apr 22 '23 at 07:28
  • I am not one of those who thinks this makes sense. For one thing const_iterator and iterator are conceptually wildly different things Having a const std::array and a non-const std::array are significant, and have an impact on the overall design. Hiding it might be convenient under the argument that you can now change the constness of arr without changing anything else --- which is a dangerously wrong conclusion to draw IMO (And i might have misread the first word, you are suggesting an alternative approach -- still disagree though in ths context) – user2846855 Apr 22 '23 at 08:03
  • I actually want the oposite, I want the member array to be non-writable. When I use init list, all good. When I use function to initiate it, I get the ability to write to the array, which should not be the case since it's static const – G Stepanovic Apr 22 '23 at 18:32
  • The return type (iterator) allows writing: Use: using const_iterator = typename std::array::const_iterator ; inline constexpr const_iterator begin() { return arr.begin(); } inline constexpr const_iterator end() { return arr.end(); } As to why your original code compiles I don't know, (UB as has been commented) – user2846855 Apr 23 '23 at 08:46
0

You may code like this:

{
    // In your class
    static constexpr std::array<int, 4> HalfCircleStatic_init();
}

constexpr std::array<int, 4> ArrayContainer::HalfCircleStatic_init() {
    std::array<int, 4> retVal{};
    for (int i = 0; i < 4; ++i) retVal[i] = i+1;
    return retVal;
}

The reason why SEGFAULT will be triggered is that you're writing to the read-only session of the program, and the read-only property is determined before runtime. By providing numbers directly, they're often hard-coded by the compiler in the program, i.e. your binary program directly contains 1, 2, 3, 4, and when the program is loaded, they will be put on the read-only pages by the OS; but by using a function, the initialization is in fact a runtime thing when there is no optimization, so the compiler has to put your const array to a writable session(else the initialization itself will trigger SEGFAULT, right?). What the code above does is just move the runtime thing to the compilation time(by constexpr), so that the compiler will try to calculate it and then may hard-code it in the binary program. If you want to force the compiler to do it and report an error if it fails, consteval can do that.

However, for float arrays, compilers are not able to calculate them in the compilation time. Keeping const is the soundest way to protect the read-only data through compilers, and you should not depend on SEGFAULT to debug. For example, you may return a cbegin()-like thing to terminate the writing behavior before running rather than after running.

o_oTurtle
  • 1,091
  • 3
  • 12
  • Using consexpr would be perfect, however in reality I need to initiate the array using cmath functions, and that won't work. – G Stepanovic Apr 22 '23 at 18:34
0

OK, seems this might work (in a way). Initialising static const member array using lambda:

const std::array<int, 4> ArrayContainer::arr = {
        [] {
            std::array<int, 4> retVal{};
            for (int i = 0; i < 4; ++i) retVal[i] = i + 1;
            return retVal;

        }() };

This is consistent, in that it produces segfault when I try to write.

  • 1
    I've said that **compilers are not able to calculate floating points in the compilation time**, and it's almost impossible to make it stored in the read-only session if you call some floating-point functions. See [here](https://godbolt.org/z/c934369aG) for a counterexample where I code like yours and can still write the array successfully without triggering SEGFAULT. Using lambda expression is the first version of my answer, but I edit it later since you can still write it if it's a float array. – o_oTurtle Apr 23 '23 at 00:46
  • You're a new guy here so I try my best to be nice, but refutation needs thorough evidence and it's important to do all possible tests or experiments in computer science. "Integers are right" never means "floating points are right". – o_oTurtle Apr 23 '23 at 00:56
  • The reason why integers are right is compilers' optimization and they're then calculated in compilation time and still hard-coded in the read-only session. See assembly [here](https://godbolt.org/z/PYhd1sG1o) where `arr` is coded as `.long 1`, etc. That is why I say "without optimization" in my answer; but it's impossible to predict soundly the way compilers optimize, as you've seen that possibly lambda can while member functions can't, so it's not secure to do so. – o_oTurtle Apr 23 '23 at 01:06