使用 std::array& 分配 C 样式数组的一个子部分而不违反“严格别名”并因此调用 UB?

Oli*_*ock 6 c++ strict-aliasing undefined-behavior stdarray

我可以使用 astd::array<int, N>来为 a 的部分别名int[]而不调用 UB 吗?

https://en.cppreference.com/w/cpp/container/array “此容器是一个聚合类型,其语义与将 C 样式数组 T[N] 作为其唯一非静态数据成员的结构体具有相同的语义。 ”

动机:copy下面的函数不受我的控制,需要通过引用进行单个分配。只有 a struct { int[N]; }like astd::array<int, N>才能进行那种“多对象赋值”?

这是UB吗?

还有别的办法吗?

#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);

}

Run Code Online (Sandbox Code Playgroud)

输出

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 
Run Code Online (Sandbox Code Playgroud)

编辑:更广泛的问题

感谢所有的评论/答案。根据大众的要求,我发布了更广泛的问题以获取更多背景信息。

我将分两个级别来完成。

1级

这是我想做的下一层:

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 
Run Code Online (Sandbox Code Playgroud)

ranges::remove抱怨说这ranges::view::chunk是“不可改变的”。这已在这里得到确认: https ://github.com/ericniebler/range-v3/issues/1760

所以我的下一个尝试是编写一个“分块范围”,我可以将其传递给 range::remove。我通过多种方式做到了这一点。有几个“有效”但基于 UB,包括使用 std::array<int,4> 作为“块代理”的这种方式(以及上面的 OP):


#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)  
}

Run Code Online (Sandbox Code Playgroud)

输出(根据需要但基于 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 
Run Code Online (Sandbox Code Playgroud)

2级

之所以非常热衷于使用范围,是因为我想要的算法还有另一层,因为实际上有一个一维并行向量,我将其与分块向量压缩在一起,然后删除条件基于一维向量。

请注意,这里两个向量都相当大(~100-500k 项),所以我想避免制作副本。这就是为什么我不使用|组合和惰性ranges::views::filter,而是使用渴望 ranges::remove来修改原始容器(两者都需要修改)。

下面的代码“对我有用”,但包含根据OP的UB:


#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);

Run Code Online (Sandbox Code Playgroud)

输出(根据需要但基于 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
Run Code Online (Sandbox Code Playgroud)

当前最佳替代方案

我当前最好的替代方案是使用处理“chunk 4”逻辑的混乱原始循环来手动编写“zip”和“remove”代码。下面是其中的一个版本,它基本上是实现的修改版本std::remove

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 
Run Code Online (Sandbox Code Playgroud)

输出(根据需要且无 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 
Run Code Online (Sandbox Code Playgroud)

我还没有检查过,但我怀疑为此“原始迭代器循环”版本生成的汇编代码至少与任何基于替代方案的版本一样好range;可能更好。

Ran*_*tep 3

正如您所注意到的,chunked_view是不可变的。您可以做的是adjacent<N> | stride(n)创建一个可变的分块视图(假设N在编译时已知):

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());
Run Code Online (Sandbox Code Playgroud)

现在data将是1,2,3,4,5,6,7,8,13,14,15,16


需要注意的一件事是adjacent创建所有元素的引用元组,因此您无法迭代元组。但是,您可以使用第一个元素的地址 和 来创建span或,因为基础数据是连续的:views::countedchunk_size

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

如果需要,您还可以删除所有临时视图,因为从删除函数中您需要的只是删除了多少个元素:

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);
Run Code Online (Sandbox Code Playgroud)