简体   繁体   中英

Assigning a subsection of C-style array using a std::array& without violating "strict aliasing" and hence invoking UB?

Can I use a std::array<int, N> to alias parts of a int[] without invoking UB?

https://en.cppreference.com/w/cpp/container/array "This container is an aggregate type with the same semantics as a struct holding a C-style array T[N] as its only non-static data member."

Motivation: The copy function below is not under my control and needs to make a single assignment via references. Only a struct { int[N]; } struct { int[N]; } like a std::array<int, N> can make that kind of "multiple object assignment"?

Is this UB?

Is there another way?

#include <iostream>
#include <array>

template <std::size_t N>
void print(int (&arr)[N], std::size_t number_rows, std::size_t number_cols) {
    assert(number_rows * number_cols == N);
    for (std::size_t r = 0; r != number_rows; ++r) {
        for (std::size_t c = 0; c != number_cols; ++c) {
            std::cout << arr[r * number_cols + c] << ' ';
        }
        std::cout << '\n';
    }
    std::cout << '\n';
}

void copy(std::array<int, 4>& a, std::array<int, 4>& b) {
  b = a;
}


int main() {

    int vals[16] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16};

    print(vals, 4, 4);

    auto s1 = reinterpret_cast<std::array<int, 4>*>(&vals[0]);
    auto s2 = reinterpret_cast<std::array<int, 4>*>(&vals[4]);

    copy(*s2, *s1);

    print(vals, 4, 4);

}

Output

1 2 3 4 
5 6 7 8 
9 10 11 12 
13 14 15 16 

5 6 7 8 
5 6 7 8 
9 10 11 12 
13 14 15 16 

Edit: The Wider problem

Thanks for all the comments / answers. By popular request I am posting the wider problem for more context.

I am going to do that in 2 levels.

Level 1

This is the next layer out of what I would like to do:


#include "range/v3/algorithm/remove.hpp"
#include "range/v3/view/chunk.hpp"
#include <vector>

int main() {

    std::vector<int> v{
        1, 2, 3, 4,
        5, 6, 7, 8, 
        9, 10, 11, 12,
        13, 14, 15, 16
    };

    auto chunked = ranges::views::chunk(v, 4);
    auto it = ranges::remove(chunked, 9, [](const auto& e) { return e[0]; }); // <== compile error

    // expected result  (xx = unspecified)
    // std::vector<int> v{
    //     1, 2, 3, 4,
    //     5, 6, 7, 8, 
    //     13, 14, 15, 16,
    //     xx, xx, xx, xx
    // };
    // and it pointing at chunked[3] (ie the row of xx)  
}

But ranges::remove complains that the ranges::view::chunk is "not permutable". This was confirmed here: https://github.com/ericniebler/range-v3/issues/1760

So my next attempt was writing a "chunked range" which I could pass to ranges::remove. I did this in multiple ways. Several "worked" but are based on UB, including this way of using std::array<int,4> as a "chunk proxy" (and hence the OP above):


#include "range/v3/algorithm/remove.hpp"
#include "range/v3/view/chunk.hpp"
#include "range/v3/view/zip.hpp"
#include <iostream>
#include <iterator>
#include <vector>

class Integers {

  public:
    struct Iterator {
        using chunk             = std::array<int, 4>;
      
        using iterator_category = std::forward_iterator_tag;
        using difference_type   = std::ptrdiff_t;
        using value_type        = chunk;
        using pointer           = value_type*;
        using reference         = value_type&;
template <class ForwardIt, class UnaryPredicate, class ChunkedForwardIt>
ForwardIt remove_if_par(ForwardIt first, ForwardIt last, UnaryPredicate p,
    ChunkedForwardIt chunked_first, std::ptrdiff_t chunk_size) {
    auto first_orig = first;
    first           = std::find_if(first, last, p);
    // advance chunked_first in lockstep. TODO this is linear compelxity unless random_access_iter
    std::advance(chunked_first, std::distance(first_orig, first) * chunk_size);
    if (first != last) {
        ForwardIt        i       = first;
        ChunkedForwardIt chunk_i = chunked_first;
        while (++i != last) {
            std::advance(chunk_i, chunk_size);
            if (!p(*i)) {
                *first++ = std::move(*i);
                // move chunk
                auto loop_chunk_i = chunk_i;
                for (std::ptrdiff_t ci = 0; ci != chunk_size; ++ci)
                    *chunked_first++ = std::move(*loop_chunk_i++);
            }
        }
    }
    return first;
}

        Iterator();
        Iterator(int* ptr) : current_row_(reinterpret_cast<chunk*>(ptr)) {}  // <== UB here

        reference operator*() const { return *current_row_; }
        pointer   operator->() { return current_row_; }
        Iterator& operator++() {
            ++current_row_;
            return *this;
        }
        Iterator operator++(int) {
            Iterator tmp = *this;
            ++(*this);
            return tmp;
        }

        friend bool operator==(const Iterator& a, const Iterator& b) {
            return a.current_row_ == b.current_row_;
        }
        friend bool operator!=(const Iterator& a, const Iterator& b) {
            return a.current_row_ != b.current_row_;
        }

      private:
        chunk* current_row_;
    };

    Iterator begin() { return Iterator(&data_[0]); }
    Iterator end() { return Iterator(&data_[16]); }

    int data_[16];
};

template <std::size_t N>
void print(int (&arr)[N], std::size_t number_rows, std::size_t number_cols) {
    assert(number_rows * number_cols == N);
    for (std::size_t r = 0; r != number_rows; ++r) {
        for (std::size_t c = 0; c != number_cols; ++c) {
            std::cout << arr[r * number_cols + c] << ' ';
        }
        std::cout << '\n';
    }
    std::cout << '\n';
}

int main() {
    Integers chunked{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16};
    print(chunked.data_, 4, 4);

    auto it = ranges::remove(chunked, 9, [](const auto& e) { return e[0]; });

    print(chunked.data_, 4, 4);

Output (as desired but based on UB)

1 2 3 4 
5 6 7 8 
9 10 11 12 
13 14 15 16 

1 2 3 4 
5 6 7 8 
13 14 15 16 
13 14 15 16 

Level 2

The reason for being quite keen on using ranges, is because there is another layer outwards for my desired algorithm, in that there is actually a 1D parallel vector which I zipped together with the chunked one and then the remove condition is based on the 1D vector.

Note that both vectors are reasonably large here (~100-500k items), so I want to avoid making a copy . This is why I am not using | composition and the lazy ranges::views::filter , but using the eager ranges::remove instead which modifies the original containers (both need modifying).

The code below "works for me", but contains the UB as per OP:


#include "range/v3/algorithm/remove.hpp"
#include "range/v3/view/zip.hpp"
#include <cstddef>
#include <iostream>
#include <iterator>
#include <vector>

class Integers {

  public:
    struct Iterator {
        using chunk             = std::array<int, 4>;
      
        using iterator_category = std::random_access_iterator_tag; // some requirements ommitted for brevity
        using difference_type   = std::ptrdiff_t;
        using value_type        = chunk;
        using pointer           = value_type*;
        using reference         = value_type&;

        Iterator();
        Iterator(int* ptr) : current_row_(reinterpret_cast<chunk*>(ptr)) {}

        reference operator*() const { return *current_row_; }
        pointer   operator->() { return current_row_; }
        Iterator& operator++() {
            ++current_row_;
            return *this;
        }
        Iterator operator++(int) {
            Iterator tmp = *this;
            ++(*this);
            return tmp;
        }

        friend std::ptrdiff_t operator-(const Iterator& lhs, const Iterator& rhs) {
            return lhs.current_row_ - rhs.current_row_;
        }
        friend bool operator==(const Iterator& a, const Iterator& b) {
            return a.current_row_ == b.current_row_;
        }
        friend bool operator!=(const Iterator& a, const Iterator& b) {
            return a.current_row_ != b.current_row_;
        }

      private:
        chunk* current_row_;
    };

    Iterator begin() { return Iterator(&data_[0]); }
    Iterator end() { return Iterator(&data_[16]); }

    // fake the initialisation for brevity
    std::vector<int> data_{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16};
};

void print(const auto& zipped) {
    for (const auto& t: zipped) {
        for (auto i: t.first) std::cout << i << ' ';
        std::cout << " | " << t.second << '\n';
    }
    std::cout << '\n';
}

// no control over this api
void external_api(int* /* ptr */, std::size_t /* size */) {}

int main() {
    Integers chunked;

    std::vector<int> b{10, 20, 30, 40};

    auto zipped = ranges::views::zip(chunked, b);
    print(zipped);
    
    auto it = ranges::remove(zipped, 30, [](const auto& e) { return e.second; });

    auto relidx = it - zipped.begin();
    chunked.data_.erase(chunked.data_.begin() + relidx * 4, chunked.data_.end());
    b.erase(b.begin() + relidx, b.end());

    print(zipped);

    external_api(&chunked.data_[0], chunked.data_.size());
    
}

Output (as desired but based on UB):

1 2 3 4  | 10
5 6 7 8  | 20
9 10 11 12  | 30
13 14 15 16  | 40

1 2 3 4  | 10
5 6 7 8  | 20
13 14 15 16  | 40

Current best alternative

My best current alternative is to hand code both "zip" and "remove" using messy raw loops that deal with the "chunk 4" logic. Below is one version of this, which is basically a modified version of the std::remove implementation:

// no control over this api
void external_api(int* /* ptr */, std::size_t /* size */) {}

template <class ForwardIt, class UnaryPredicate, class ChunkedForwardIt>
ForwardIt remove_if_par(ForwardIt first, ForwardIt last, UnaryPredicate p,
    ChunkedForwardIt chunked_first, std::ptrdiff_t chunk_size) {
    auto first_orig = first;
    first           = std::find_if(first, last, p);
    // advance chunked_first in lockstep. TODO this is linear compelxity unless random_access_iter
    std::advance(chunked_first, std::distance(first_orig, first) * chunk_size);
    if (first != last) {
        ForwardIt        i       = first;
        ChunkedForwardIt chunk_i = chunked_first;
        while (++i != last) {
            std::advance(chunk_i, chunk_size);
            if (!p(*i)) {
                *first++ = std::move(*i);
                // move chunk
                auto loop_chunk_i = chunk_i;
                for (std::ptrdiff_t ci = 0; ci != chunk_size; ++ci)
                    *chunked_first++ = std::move(*loop_chunk_i++);
            }
        }
    }
    return first;
}

void print(const std::vector<int>& a, const std::vector<int>& chunked, std::size_t chunk_size) {
    for (std::size_t i = 0; i != a.size(); ++i) {
        std::cout << a[i] << " | ";
        for (std::size_t j = 0; j != chunk_size; ++j)
            std::cout << chunked[i * chunk_size + j] << ' ';
        std::cout << '\n';
    }
    std::cout << '\n';
}

int main() {
    std::vector<int> a{10, 20, 30, 40, 50, 60, 70};
    std::vector<int> chunked{1,  2,  3,  4,  5,  6,  7,  8,  9,  10, 11, 12, 13, 14,
        15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28};

    static constexpr std::ptrdiff_t chunk_size = 4;

    print(a, chunked, chunk_size);
    
    auto it = remove_if_par(
        a.begin(), a.end(), [](auto e) { return e % 20 == 0; }, chunked.begin(), chunk_size);

    print(a, chunked, chunk_size);
    
    a.erase(it, a.end());
    chunked.erase(chunked.begin() + (it - a.begin()) * chunk_size, chunked.end());

    print(a, chunked, chunk_size);
    
    external_api(&chunked[0], chunked.size());
}

Output (as desired and without UB)

10 | 1 2 3 4 
20 | 5 6 7 8 
30 | 9 10 11 12 
40 | 13 14 15 16 
50 | 17 18 19 20 
60 | 21 22 23 24 
70 | 25 26 27 28 

10 | 1 2 3 4 
30 | 9 10 11 12 
50 | 17 18 19 20 
70 | 25 26 27 28 
50 | 17 18 19 20 
60 | 21 22 23 24 
70 | 25 26 27 28 

10 | 1 2 3 4 
30 | 9 10 11 12 
50 | 17 18 19 20 
70 | 25 26 27 28 

I haven't checked, but I suspect that the assembly code generated for this "raw iterator loop" version is at least as good as any range based alternative; probably better.

Can I use a std::array<int, N> to alias parts of a int[] without invoking UB?

No.

Is this UB?

Yes.

Is there another way?

Depends on what parts of your scenario you can change.

The copy function below is not under my control

Simplest solution would be to not use the copy function that's not useful for your use case.

the std::array<int, N>& is the return value of the operator*() of a custom iterator over the raw int[]

Seems that there's probably no good way to implement such operator*() .

This seems like a good opportunity to define a custom range adaptor with custom assignment operators that you could return from the custom iterator.

As you have noticed, chunked_view is not mutable. What you can do instead is to use adjacent<N> | stride(n) adjacent<N> | stride(n) to create a mutable chunked view(assuming N is known at compile time):

std::vector data {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16};
std::vector b{10, 20, 30, 40};
constexpr std::size_t chunk_size = 4;

auto chunked_view = data 
    | std::views::adjacent<chunk_size>
    | std::views::stride(chunk_size);
auto zipped_view = std::views::zip(chunked_view, b);

auto removed_range = std::ranges::remove(zipped_view, 30, [](auto pair){ return std::get<1>(pair); });

data.resize(data.size() - removed_range.size() * chunk_size);
b.resize(b.size() - removed_range.size());

Now data will be 1,2,3,4,5,6,7,8,13,14,15,16 .


One thing to note is that adjacent creates a tuple of references of all the elements, so you can't iterate through a tuple. However, you can create a span or views::counted by using the address of the first element and chunk_size , since the underlying data were contiguous:

for (auto& [tuple, key] : zipped_view) {
    for (auto i : std::span(&std::get<0>(tuple), chunk_size)) std::cout << i << ' ';
    std::cout << " | " << key << '\n';
}

You can also remove all the temporary views if desired, since all you needed from the remove function is how many elements were removed:

std::vector data {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16};
std::vector b{10, 20, 30, 40};
constexpr std::size_t chunk_size = 4;

auto removal_size = 
    std::ranges::remove(
        std::views::zip(
            data | std::views::adjacent<chunk_size> | std::views::stride(chunk_size)
        , b) 
        , 30, [](auto pair){ return std::get<1>(pair); }
    ).size();

data.resize(data.size() - removal_size * chunk_size);
b.resize(b.size() - removal_size);

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM