Empresas
Empleos
  • Sobre nosotros
  • Soluciones
    • Publicación de vacantes
      Publica tu vacante y recibe candidatos calificados en 48h.
    • Evaluación de candidatos
      500+ pruebas técnicas y psicológicas, más anti-fraude.
    • Headhunting
      Búsqueda ejecutiva a la medida de principio a fin.
    • Nómina + EOR
      Dispersión de nómina y EOR en más de 15 países de LATAM.
  • Precios
  • Empleos

0

387
Vistas
¿Copiar matrices 2D con "memcpy" es un comportamiento técnicamente indefinido?

Ha surgido una discusión interesante en los comentarios a esta pregunta reciente : ahora, aunque el lenguaje allí es C , la discusión se ha desviado a lo que especifica el estándar C++ , en términos de lo que constituye un comportamiento indefinido al acceder a los elementos de una matriz multidimensional utilizando un funciona como std::memcpy .

Primero, aquí está el código de esa pregunta, convertido a C++ y usando const siempre que sea posible:

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

El problema está en la llamada a std::memcpy : el argumento arr generará (por decaimiento) un puntero al primer int[3] , por lo que, según un lado de la discusión (dirigida por Ted Lyngmo ), cuando el memcpy función accede a datos más allá del tercer elemento de ese subarreglo, hay un comportamiento formalmente indefinido (y lo mismo se aplicaría al destino, arr_copy ).

Sin embargo, el otro lado del debate (al que mediocrevegetable1 y yo nos suscribimos) utiliza la lógica de que cada una de las matrices 2D, por definición , ocupará memoria continua y, dado que los argumentos de memcpy son solo punteros void* a esas ubicaciones (y el tercero, el argumento de size es válido), entonces no puede haber UB aquí.

Aquí hay un resumen de algunos de los comentarios más pertinentes para el debate, en caso de que se produzca alguna "limpieza" en la pregunta original (negrita para énfasis mío):

No creo que haya ningún fuera de los límites aquí. Al igual que memcpy funciona para una matriz de int s, funciona para una matriz de int [3] s, y ambos deben ser contiguos (pero no estoy 100 % seguro). – mediocrevegetal1

El acceso fuera de los límites ocurre cuando copia el primer byte de arr[0][3] . Nunca lo he visto fallar, pero, en C++, tiene UB. – Ted Lyngmo

Pero la función/llamada memcpy no realiza ninguna indexación de matriz: solo se le dan dos punteros void* y copia la memoria de uno a otro. – Adrián Mole

No puedo decir con seguridad si eso importa en C. En C++ no es así. Obtiene un puntero al primer int[3] y cualquier acceso fuera de su rango tiene UB. No he encontrado ninguna excepción a eso en el estándar C++. – Ted Lyngmo

No creo que se aplique lo arr[0][3] . Según esa lógica, creo que copiar el segundo int de una matriz int a través memcpy también sería UB. int [3] es simplemente el tipo de elementos de arr , y los límites de arr como un todo en bytes deben ser sizeof (int [3]) * 3 . Aunque probablemente me esté perdiendo algo :/ – mediocrevegetable1

¿Hay algún abogado de lenguaje C++ que pueda resolver el asunto, preferiblemente con (una) cita(s) apropiada(s) del estándar C++?

Además, las citas relevantes del estándar C pueden ser útiles, especialmente si los estándares de los dos idiomas difieren, por lo que he incluido la etiqueta C en esta pregunta.

over 4 years ago · Santiago Trujillo
4 Respuestas
Responde la pregunta

0

Mi punto de vista actual es que cuando se pasa un int[3][3] como argumento a una función, se convierte en un puntero al primer elemento de esa matriz. El primer elemento es un int[3] y los otros dos int[3] están dentro del rango, al igual que cuando pasas un int[3] 1D a una función, obtienes un puntero al primer int y a los otros dos int s están dentro del rango, por lo tanto, el memcpy es seguro.


Respuesta original:

Esta respuesta se basa en algunas suposiciones incorrectas que hice al leer algo hace mucho tiempo. Dejaré la respuesta y los comentarios para quizás evitar que otras personas caigan en la misma trampa mental.

Lo que se pasa a la función decae en punteros a los primeros elementos, es decir, en este caso, dos int(*)[3] s.

C borrador del Anexo J (informativo) Problemas de portabilidad J.2 Comportamiento indefinido :

Un subíndice de matriz está fuera de rango, incluso si un objeto es aparentemente accesible con el subíndice dado (como en la expresión lvalue a[1][7] dada la declaración int a[4][5] ) (6.5.6).

memcpy(arr_copy, arr, sizeof arr); get's two int(*)[3] y accederá a ambos fuera de rango, por lo tanto, UB.

over 4 years ago · Santiago Trujillo Denunciar

0

El estándar C++ dice ( [cstring.syn]/1 ):

El contenido y el significado del encabezado <cstring> son los mismos que el encabezado de la biblioteca estándar de C <string.h> .

C11 7.24.2.1 La función memcpy dice:

Sinopsis

1

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

Descripción

2 La función memcpy copia n caracteres del objeto señalado por s2 en el objeto señalado por s1 ...

Dada esta descripción, cabe preguntarse qué pasa si n es mayor que el tamaño del objeto al que apunta s1 / s2 . El «sentido común» sugiere que copiar más de, digamos, tamaño de sizeof(int) bytes de un objeto int no debería tener sentido.

Y, de hecho, hay 7.24.1 Convenciones de funciones de cadenas p.1 que dicen:

El encabezado <string.h> declara un tipo y varias funciones, y define una macro útil para manipular matrices de tipo de carácter y otros objetos tratados como matrices de tipo de carácter . … Se utilizan varios métodos para determinar las longitudes de las matrices, pero en todos los casos un argumento char * o void * apunta al carácter inicial (dirección más baja) de la matriz. Si se accede a una matriz más allá del final de un objeto, el comportamiento es indefinido .

Así, al pasar un puntero al primer elemento de un array, es «el objeto» de memcpy p.2 y tratar de copiar más bytes de los que tiene este objeto es UB.

over 4 years ago · Santiago Trujillo Denunciar

0

Con el debido respeto, HolyBlackCat está completamente equivocado, en principio. Mi borrador estándar C17 dice en 7.24.1: "Para todas las funciones en esta subcláusula [que contiene memcpy], cada carácter se interpretará como si tuviera el tipo char sin firmar". El estándar C realmente no hace ninguna consideración de tipo para estas funciones triviales: memcpy copia la memoria. En lo que respecta a la semántica, se trata como una secuencia de caracteres sin firmar. Por lo tanto, se aplica el siguiente primer principio C:

Siempre que haya un objeto inicializado en una dirección, puede acceder a él a través de un puntero de caracteres.

Repitámoslo para énfasis y claridad:

Se puede acceder a cualquier objeto inicializado mediante un puntero char.

Si sabe que un objeto está en una dirección específica 0x42, por ejemplo porque el hardware de su computadora asigna la coordenada x de su mouse allí, puede convertir eso en un puntero de caracteres y leerlo. Si la coordenada es un valor de 16 bits, también puede leer el siguiente byte.

A nadie le importa cómo sabes que hay un número entero: si hay uno, puedes leerlo. (Peter Cordes señaló que no hay garantía de que pueda llegar a una dirección válida (o al menos, a la dirección esperada) a través de la aritmética de punteros de un objeto no relacionado debido a las posibles arquitecturas de memoria segmentada. Pero este no es el caso del ejemplo: el toda la matriz es un objeto y debe residir en un solo segmento).

Ahora que tenemos 3 matrices de 3 entradas, sabemos que 9 entradas se colocan consecutivamente en la memoria; eso es un requisito de idioma. Toda la memoria está llena de ints que pertenecen a un solo objeto, y podemos iterar manualmente sobre él a través de punteros char, o podemos convertirlo en memcpy. Ya sea que usemos arr o arr[0] u obtengamos la dirección a través de un desplazamiento de pila de alguna otra variable [<- no se garantiza que sea correcto como me recordó Peter Cordes] o alguna otra magia o simplemente hacemos una suposición educada es completamente irrelevante siempre que el dirección es correcta, y de eso no hay duda aquí.

over 4 years ago · Santiago Trujillo Denunciar

0

El uso indicado de memcpy será procesado significativamente por cualquier compilador cuyos autores no abusen del Estándar como excusa para considerar construcciones útiles como "rotas". Las únicas personas a las que debería importarles si el Estándar realmente lo define sin contradicción serían los compiladores que abusan del Estándar, o aquellos que buscan protegerse contra los compiladores que abusan del Estándar. Si los estándares C o C++ estaban destinados a ser inmunes al abuso, podría valer la pena preocuparse por si especifican de forma 100 % inequívoca todos los casos en los que memcpy debería funcionar. Ambos están escritos, sin embargo, para confiar en que los escritores de compiladores reconozcan que si un estándar especifica simultáneamente cómo funcionan algunas construcciones, pero caracteriza un conjunto de construcciones superpuestas como invocando un comportamiento indefinido, los compiladores deben hacer un esfuerzo de buena fe para procesar el código de la manera más útil. como práctico.

Considere las dos funciones:

 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]; }

Dependiendo de lo que el programador intente hacer, cualquiera de las siguientes interpretaciones podría ser más útil:

  1. Procese ambas funciones de forma que vuelva a cargar el valor de arr[1][0][0] después de memcpy .

  2. Procese ambas funciones de una manera que devuelva 1 incondicionalmente sin tener en cuenta si memcpy lo sobrescribió.

  3. Procese la primera función de una manera que incondicionalmente devuelva 1, pero procese la segunda de una manera que vuelva a cargar arr[1][0][0] , sobre la base de que mientras los Estándares definen el uso de operadores de índice en matrices lvalues/glvalues En términos de la descomposición de la matriz seguida de la indexación del puntero, la elección de sintaxis de los programadores a menudo se basa en si un valor lvalue/glvalue de tipo matriz se usa realmente como una matriz o se usa como un medio para obtener un puntero al primer elemento. , que luego se utilizará como base para otros cálculos de direcciones.

Si un compilador intentara procesar el código de manera significativa en el caso en que i y el mode son 4, no habría ninguna ambigüedad de buena fe sobre cómo debería comportarse el código. Sólo un comportamiento tendría sentido. La única ambigüedad es si los beneficios de acomodar ese caso valdrían el costo de ejecución de hacerlo; acomodar el comportamiento siempre es una opción "segura". Sería incómodo escribir el Estándar para decir que test1 debería tener un comportamiento definido para i==0..3 cuando n es 4, e i==0..4 cuando n es cero, pero test2 debería tener un comportamiento definido para i==0..15 independientemente de n , pero para la mayoría de los propósitos, la mejor combinación de semántica, compatibilidad y optimización se lograría procesando el código de esa manera.

over 4 years ago · Santiago Trujillo Denunciar
Responde la pregunta
Encuentra empleos remotos

¡Descubre la nueva forma de encontrar empleo!

Top de empleos
Top categorías de empleo
Empresas
Publicar vacante Precios Comercial
Legal
Términos y condiciones Política de privacidad
© 2026 PeakU Inc. All Rights Reserved.
Andres GPT
Recomiéndame algunas ofertas
Necesito ayuda