Business
Jobs
  • About Us
  • Solutions
    • Job Postings
      Post your job and receive qualified candidates in 48h.
    • Candidate Assessments
      500+ technical and psychological tests, plus anti-fraud.
    • Headhunting
      Tailor-made executive search from start to finish.
    • Payroll + EOR
      Payroll dispersal and EOR across 15+ LATAM countries.
  • Pricing
  • Jobs

0

396
Views
Is copying 2D arrays with "memcpy" technically undefined behaviour?

An interesting discussion has arisen in the comments to this recent question: Now, although the language there is C, the discussion has drifted to what the C++ Standard specifies, in terms of what constitutes undefined behaviour when accessing the elements of a multidimensional array using a function like std::memcpy.

First, here's the code from that question, converted to C++ and using const wherever possible:

#include <iostream>
#include <cstring>

void print(const int arr[][3], int n)
{
    for (int r = 0; r < 3; ++r) {
        for (int c = 0; c < n; ++c) {
            std::cout << arr[r][c] << " ";
        }
        std::cout << std::endl;
    }
}

int main()
{
    const int arr[3][3] = { {1, 2, 3}, {4, 5, 6}, {7, 8, 9} };
    int arr_copy[3][3];
    print(arr, 3);
    std::memcpy(arr_copy, arr, sizeof arr);
    print(arr_copy, 3);
    return 0;
}

The issue is in the call to std::memcpy: the arr argument will yield (by decay) a pointer to the first int[3] subarray so, according to one side of the discussion (led by Ted Lyngmo), when the memcpy function accesses data beyond the third element of that subarray, there is formally undefined behaviour (and the same would apply to the destination, arr_copy).

However, the other side of the debate (to which mediocrevegetable1 and I subscribe) uses the rationale that each of the 2D arrays will, by definition, occupy continuous memory and, as the arguments to memcpy are just void* pointers to those locations (and the third, size argument is valid), then there cannot be UB here.

Here's a summary of some of the comments most pertinent to the debate, in case any "clean-up" occurs on the original question (bolding for emphasis mine):

I don't think there's any out-of-bounds here. Just like memcpy works for an array of ints, it works for an array of int [3]s, and both should be contiguous (but I'm not 100% sure). – mediocrevegetable1

The out of bounds access happens when you copy the first byte from arr[0][3]. I've never seen it actually fail, but, in C++, it has UB. – Ted Lyngmo

But the memcpy function/call doesn't do any array indexing - it's just given two void* pointers and copies memory from one to the other. – Adrian Mole

I can't say for sure if that matters in C. In C++ it doesn't. You get a pointer to the first int[3] and any access out of its range has UB. I haven't found any exception to that in the C++ standard. – Ted Lyngmo

I don't think the arr[0][3] thing applies. By that logic, I think copying the second int of an int array through memcpy would be UB as well. int [3] is simply the type of arr's elements, and the bounds of arr as a whole in bytes should be sizeof (int [3]) * 3. I'm probably missing something though :/ – mediocrevegetable1

Are there any C++ Language-Lawyers who can settle the matter – preferably with (an) appropriate citation(s) from the C++ Standard?

Also, relevant citations from the C Standard may be helpful – especially if the two language Standards differ – so I've included the C tag in this question.

over 4 years ago · Santiago Trujillo
4 answers
Answer question

0

My current standpoint is that when an int[3][3] is passed as an argument to a function, it decays into a pointer to the first element in that array. The first element is an int[3] and the other two int[3]s are in range - just like when you pass a 1D int[3] to a function, you get a pointer to the first int and the other two ints are in range, hence the memcpy is safe.


Original answer:

This answer is based on some wrong assumptions I made by reading something a long time ago. I'll leave the answer and the comments up to perhaps prevent other people from walking into the same mind-trap.

What is passed to the function decays into a pointers to the first elements, that is in this case, two int(*)[3]s.

C draft Annex J (informative) Portability issues J.2 Undefined behavior:

An array subscript is out of range, even if an object is apparently accessible with the given subscript (as in the lvalue expression a[1][7] given the declaration int a[4][5]) (6.5.6).

memcpy(arr_copy, arr, sizeof arr); get's two int(*)[3] and will access both out of range, hence, UB.

over 4 years ago · Santiago Trujillo Report

0

C++ standard says ([cstring.syn]/1):

The contents and meaning of the header <cstring> are the same as the C standard library header <string.h>.

C11 7.24.2.1 The memcpy function says:

Synopsis

1

         #include <string.h>
         void *memcpy(void * restrict s1,
              const void * restrict s2,
              size_t n);

Description

2 The memcpy function copies n characters from the object pointed to by s2 into the object pointed to by s1…

Given this description, one may wonder what if n is greater than the size of the object pointed to by s1/s2. «Common sense» suggests that copying more than, say, sizeof(int) bytes from an int object should be meaningless.

And indeed, there is 7.24.1 String function conventions p.1 saying:

The header <string.h> declares one type and several functions, and defines one macro useful for manipulating arrays of character type and other objects treated as arrays of character type. … Various methods are used for determining the lengths of the arrays, but in all cases a char * or void * argument points to the initial (lowest addressed) character of the array. If an array is accessed beyond the end of an object, the behavior is undefined.

Thus, when passing a pointer to the first element of an array, it is «the object» from memcpy p.2 and trying to copy more bytes than this object has is UB.

over 4 years ago · Santiago Trujillo Report

0

With all due respect, HolyBlackCat is utterly wrong, for very first principles. My C17 standard draft says in 7.24.1: "For all functions in this subclause [containing memcpy], each character shall be interpreted as if it had the type unsigned char." The C standard doesn't really make any type considerations for these trivial functions: memcpy copies memory. As far as semantics are at all considered, it is treated as a sequence of unsigned characters. Therefore, the following first C principle applies:

As long as there is an initialized object at an address you can access it through a char pointer.

Let's repeat it for emphasis and clarity:

Any initialized object can be accessed by a char pointer.

If you know that an object is at a specific address 0x42, for example because the hardware of your computer maps the x coordinate of your mouse there, you can convert that into a char pointer and read it. If the coordinate is a 16 bit value you can read the next byte too.

Nobody cares how you know that there is an integer: If there is one, you can read it. (Peter Cordes noted that there is no guarantee that you can arrive at a valid address (or at least, at the expected address) through pointer arithmetic from an unrelated object because of possible segmented memory architectures. But this is not the example case: The entire array is one object and must reside in a single segment.)

Now that we have 3 arrays of 3 ints we know that 9 ints are placed consecutively in memory; that is a language requirement. The entire memory there is full of ints belonging to a single object, and we can iterate manually over it through char pointers, or we can turf it to memcpy. Whether we use arr or arr[0] or obtain the address through a stack offset from some other variable [<- not guaranteed correct as Peter Cordes reminded me] or some other magic or simply make an educated guess is entirely irrelevant as long as the address is correct, and of that there is no doubt here.

over 4 years ago · Santiago Trujillo Report

0

The indicated use of memcpy will be processed meaningfully by any compiler whose authors don't abuse the Standard as an excuse to regard useful constructs as "broken". The only people who should care about whether the Standard actually defines it without contradiction would be compiler writers who abuse the Standard, or those seeking to protect themselves against compiler writers that abuse the Standard. If the C or C++ Standards were intended to be immune to abuse, it might be worth worrying about whether it 100% unambiguously specifies all of the cases were memcpy should work. Both are written, however, to be reliant upon compiler writers to recognize that if a Standard would simultaneously specifies how some constructs work, but characterizes an overlapping set of constructs as invoking Undefined Behavior, compilers should make a good faith effort to process code as usefully as practical.

Consider the two functions:

char arr[4][4][4];

int test1(int i, unsigned mode)
{
  arr[1][0][0] = 1;
  memcpy(arr[0][i], arr[2][0], mode & 4);
  return arr[1][0][0];
}

int test2(int i, unsigned mode)
{
  arr[1][0][0] = 1;
  memcpy(arr[0]+i, arr[2], mode & 4);
  return arr[1][0][0];
}

Depending upon what the programmer is trying to do, any of the following interpretations might be most useful:

  1. Process both functions in a way that reloads the value of arr[1][0][0] after the memcpy.

  2. Process both functions in a way that returns 1 unconditionally without regard for whether memcpy overwrote it.

  3. Process the first function in a manner that unconditionally returns 1, but process the second in a manner that reloads arr[1][0][0], on the basis that while the Standards define the use of index operators on array lvalues/glvalues in terms of array decay followed by pointer indexing, programmers' choice of syntax is often based upon whether an array-type lvalue/glvalue is actually being used as an array, or is being used as a means of getting a pointer to the first element, which then be used as the base for further address calculations.

If a compiler were to attempt to process the code meaningfully in the case where i and mode are 4, there would be no bona fide ambiguity about how the code should behave. Only one behavior would make sense. The only ambiguity is whether the benefits of accommodating that case would be worth the execution cost of doing so; accommodating the behavior is always a "safe" choice. It would be awkward to write the Standard to say that test1 should have defined behavior for i==0..3 when n is 4, and i==0..4 when n is zero, but test2 should have defined behavior for i==0..15 regardless of n, but for most purposes the best blend of semantics, compatibility, and optimization would be achieved by processing code in that fashion.

over 4 years ago · Santiago Trujillo Report
Answer question
Find remote jobs

Discover the new way to find a job!

Top jobs
Top job categories
Business
Post vacancy Pricing Sales
Legal
Terms and conditions Privacy policy
© 2026 PeakU Inc. All Rights Reserved.
Andres GPT
Show me some job opportunities
There's an error!