Pytanie Przekazywanie tablicy do funkcji (i dlaczego nie działa w C ++)


Natknąłem się na jakiś kod C, który się kompiluje, ale nie rozumiem dlaczego. W szczególności mam bibliotekę C, która ma dużo kodu używającego tego formatu:

void get_xu_col(int i_start,
                int n,
                double x[n],
                int n_x,
                int n_u,
                int n_col,
                double xu_col[n_col][n_x + n_u]){
    ... 
}

int main(){
    ...
    double xu_col[n_col][n_x + n_u];
    get_xu_col( ..., xu_col );
    ...
}

Czego nie rozumiem, to dlaczego kompilator pozwala na wymiarowanie w tablicach. Zgodnie z moim rozumieniem albo rozmiary muszą być ustalone (np. xu_col[9][7]) lub niezdefiniowane (np. xu_col[][]). W powyższym kodzie okazuje się, że rozmiary nie są stałymi w czasie kompilacji.

Czy kompilator po prostu ignoruje tutaj argumenty? lub czy naprawdę robi to podczas sprawdzania wymiarów?

Jeśli jest to drugie, wydaje się, że podatne na błędy jest przekazywanie wymiarów osobno.

Druga część pytania to:

Dlaczego ta sama wersja nie działa w C ++? Kiedy dosłownie zmieniam rozszerzenie pliku z .c do .cpp i próbuję przekompilować, dostaję

candidate function not viable: no known conversion from 'double [n_col][n_x + n_u]' to 'double (*)[n_x + n_u]' for 7th argument
void get_xu_col(int i_start, int n, double x[n], int n_x, int n_u, int n_col, double xu_col[n_col][n_x + n_u]);

Chciałbym wiedzieć, który idiom powinienem użyć, aby przekonwertować ten kod do C ++, ponieważ najwyraźniej poprzedni idiom był czymś, co działa w C, ale nie w C ++.


44
2018-02-21 08:46


pochodzenie


Co ciekawe, kompilator powinien zignorować wartości argumentów używanych jako rozmiary tablic, chyba że zmusisz je do rozważenia ich przez dodanie static (na przykład. double x[static n]). Pomimo patrząc tak jak robi coś (i nie działa w C ++, a wymagające wartości są poprawne / wyrażenia w zakresie), formularz w pytaniu jest nieco bezużyteczny. Irytująca dziura w języku. - Leushenko
Nie rozumiem, o czym mówisz static zrobiłby ... Dodałby słowo kluczowe static sprawiają, że jest to sprawdzanie podczas kompilacji? - bremen_matt
Istotnych: Jak korzystać z tablic w C ++?. - sbi
W przypadku, gdy jesteś nieświadomy, możliwe jest użycie kodu C w projektach C ++, bez "konwersji kodu na C ++" - M.M
@ M.M OP powiedział, że właśnie przekonwertował rozszerzenie pliku i nie udało mu się go skompilować. - a concerned citizen


Odpowiedzi:


W języku C możliwe jest użycie parametrów funkcji w celu zdefiniowania wielkości parametru tablicy o zmiennej długości, o ile wielkość jest przed tablicą na liście parametrów. To nie jest obsługiwane w C ++.


60
2018-02-21 08:53



Możesz dodać wersję, która została dodana. - Deduplicator
VLA zostały dodane w C99, ale opuściły standard (opcjonalne wsparcie) dla C11. - TWhelan


Powodem, dla którego działa w C, ale nie w C ++, jest po prostu kod C, a nie C ++. Te dwa języki mają wspólną historię, a nie gramatykę.

Metoda C ++ do przekazywania tablic o zmiennych rozmiarach to std::vectorprawdopodobnie przez odniesienie jeśli zamierzasz zmodyfikować wektor w funkcji lub przez const  odniesienie jeśli nie.


37
2018-02-21 08:50



Myślałem, że musisz użyć [*] dla parametrów funkcji tablic o zmiennej długości w C. - Bathsheba
@ Batszaba: Pierwszy raz widziałem [*] był już w kontekście wyjaśniającym dlaczego [N] tam też pracował. Ale żadna składnia nie zadziała w C ++ - jest to wynalazek C po rozwidleniu C ++. - MSalters
Wycofane, ponieważ zwykłe powtórzenie faktu podanego w pytaniu nie jest odpowiedzią na pytanie dlaczego. Podejrzewam, że odpowiedź prawdopodobnie wynika z tego, że została dodana w C99 lub później (długo po tym, jak C ++ zostało utworzone jako bliskie nadzbioru C), ale nie wiem wystarczająco dużo C, aby to było czymś więcej niż spekulacją. - Nye
@Nye: Istnieją części C99, które przekształciły się w C ++, więc to nie jest powód. Ale już C ++ std::vector od 1998 roku. Nie potrzebował tablic o zmiennej długości. - MSalters
@bremen_matt: Jak sądzisz, że wersja C może wykonać sprawdzanie w czasie kompilacji w tablicy runtime-variable-length? - DevSolar


Czego nie rozumiem, to dlaczego kompilator pozwala na wymiarowanie w tablicach. Zgodnie z moim rozumieniem, albo rozmiary muszą być ustalone (na przykład xu_col [9] [7]), albo niezdefiniowane (na przykład xu_col [] []). W powyższym kodzie okazuje się, że rozmiary nie są stałymi w czasie kompilacji.

Masz rację, rozmiary nie są stałymi w czasie kompilacji. Jeśli masz dwuwymiarową tablicę, x [linia] [col], kompilator potrzebuje liczby elementów w linii do obliczenia adresu elementu. Zobacz przykładowy kod get_char_2 () i get_char_3 ().

Jeśli korzystasz z tablic o zmiennej długości (VLA) jako parametrów funkcji, musisz podać te liczby (patrz przykład get_char_1). Możesz pisać:

 my_func( x[][width] )

lub możesz pisać

 my_func( x[999][width] )

Czy kompilator po prostu ignoruje tutaj argumenty? czy to naprawdę robi> kontrola czasu kompilacji na wymiarach?

Pierwsza liczba (999) zostanie zignorowana przez kompilator. Drugi jest potrzebny. Bez rozmiaru linii kompilator nie może obliczyć adresów wewnątrz tych tablic 2D. Kompilator nie wykonuje sprawdzeń w czasie wykonywania lub podczas kompilacji dla VLA w C.

/* file: vla.c
 *
 * variable length array example
 *
 * compile with:
 *   
 *    gcc -g -Wall -o vla vla.c 
 *
 */

#include <stdio.h>
#include <wchar.h>


/* 4 Lines - each line has 8 wide-characters */
wchar_t tab[][8] = {
{ L"12345678" },
{ L"abcdefgh" },
{ L"ijklmnop" },
{ L"qrstuvwx" }
};

/* memory layout:   
   0x00:   0x0031  0x0032 0x0033  0x0034  0x0035  0x0036  0x0037  0x0038 
   0x20:   0x0061  0x0062 0x0063  0x0064  0x0065  0x0066  0x0067  0x0068 
   ...

*/



/* get character from table w/o variable length array and w/o type */
char get_char_3(int line, int col, int width, int typesize, void *ptr )
{
char ch = * (char *) (ptr + width * typesize * line + col * typesize ); 

printf("line:%d col:%d char:%c\n", line, col, ch ); 
return ch;
}


/* get character from table w/o variable length array */
char get_char_2(int line, int col, int width, wchar_t *ptr)
{
char ch = (char) (ptr + width * line)[col]; 

printf("line:%d col:%d char:%c\n", line, col, ch ); 
return ch;
}

/* get character from table : compiler does not know line length for 
   address calculation until you supply it (width). 
*/
char get_char_1(int line, int col, int width, wchar_t aptr[][width] )
{
/* run-time calculation: 
   (width * sizeof(char) * line)  + col 
     ???    KNOWN          KOWN     KNOWN
*/
char ch = (char) aptr[line][col];

printf("line:%d col:%d char:%c\n", line, col, ch ); 
return ch;
}


int main(void)
{
char ch;

ch = tab[1][7]; /* compiler knows line length */
printf("at 1,7 we have: %c\n",  ch );

/* sizeof tab[0][0] == sizeof(wchar_t) */ 

ch = get_char_1(1,7, sizeof(tab[0])/sizeof(tab[0][0]), tab);
printf("1 returned char: %c\n", ch );

ch = get_char_2(1,7, sizeof(tab[0])/sizeof(tab[0][0]), (wchar_t*)tab);
printf("2 returned char: %c\n", ch );

ch = get_char_3(1,7, sizeof(tab[0])/sizeof(tab[0][0]),
        sizeof( wchar_t), tab);
printf("3 returned char: %c\n", ch );

printf("table size: %lu, line size: %lu,  element size: %lu\n",
       sizeof(tab),
       sizeof(tab[0]),
       sizeof(tab[0][0])
       );

printf("number of elements per lines: %lu\n",
       sizeof(tab[0])/sizeof(tab[0][0]));


printf("number of lines: %lu\n",
       sizeof(tab)/sizeof(tab[0]));

return 0;
}

12
2018-02-21 16:36





Wszystko, co robi (w języku C), pozwala na zapisanie kodu indeksującego w wywołanej funkcji bez konieczności samodzielnego obliczania adresu, na przykład:

double d= xu_col[i*row_size + j]; //get element [i,j]

przeciw

double d= xu_col[i][j];

6
2018-02-21 09:06





Gdy parametr jest zadeklarowany jako mający jednowymiarowy typ tablicy, C ignoruje podany rozmiar i zamiast tego traktuje parametr jako wskaźnik do typu elementu. W przypadku zagnieżdżonych (wielowymiarowych) tablic, taka obróbka jest stosowana tylko do zewnętrznej macierzy. W C89 wymiary wewnętrzne musiały mieć stałe rozmiary, ale w C99 wymiary mogą być wyrażeniami. Jeśli parametry, które są potrzebne do obliczenia rozmiaru tablicy, nie są wyświetlane aż do tablicy, konieczne będzie użycie ciekawej mieszanki starej i nowej składni do zadeklarowania funkcji, np.

int findNonzero(short dat[*][*], int rows, int cols);
int findNonzero(dat, rows, cols)
    int rows,cols;
    short dat[static rows][cols];
{
    for (int i=0; i<rows; i++)
        for (int j=0; j<cols; j++)
            if (dat[i][j] != 0) return i;
    return -1;
}

Zwróć uwagę, że rozmiary macierzy są określone jako * w prototypie funkcji i że definicja funkcji nie określa typów na liście argumentów, lecz opisuje wszystkie typy parametrów między listą argumentów a nawiasem otwierającym. Należy również zauważyć, że podczas gdy kompilator prawdopodobnie zignoruje liczbę wierszy w deklaracji macierzy, ale inteligentny kompilator może być w stanie go użyć, aby ułatwić optymalizację. W efekcie dziwna "statyczna" składnia zachęca kompilator do odczytania dowolnych części tablicy, aż do podanego rozmiaru, jaki uzna za odpowiedni, niezależnie od tego, czy wartości są odczytywane przez kod. Może to być pomocne na niektórych platformach, na których kod może odnieść korzyści z jednoczesnego przetwarzania wielu elementów tablicy.


6
2018-02-21 21:40





Trudność z próbką kodu polega na tym, że jeden z parametrów funkcji jest protoypowany, double xu_col[n_col][n_x + n_u], gdzie n_x i n_u są zmiennymi, a nie stałymi. Jeśli po prostu przekazujesz to jako double[] zamiast tego niektóre kompilatory C ++ mogą zezwolić na rzutowanie takie jak double (&table)[n_col][n_x + n_u] = (double(&)[n_col][n_x + n_u])xu_col; do pracy jako niestandardowe rozszerzenie, ale przenośne podejście polegałoby na pisaniu takich przejść xu_col[i*(n_x+n_u) + j], które można uprościć za pomocą funkcji pomocnika, jeśli jest to zbyt brzydkie.

Alternatywnym podejściem, prawdopodobnie bardziej zgodnym z duchem STL, może być napisanie minimalnej klasy kontenera, która zna jej wymiary, przechowuje elementy w macierzy liniowej dla wydajności. Następnie możesz zadeklarować redim_array<double> table = redim_array<double>(xu_col, n_col*(n_x+n_u)).redim(n_col, n_x+n_u); i dostęp table(i,j).

Kilka innych odpowiedzi opisuje składnię tablic o zmiennej długości, ale innym aspektem twojego pytania jest to, jak legalne jest, aby niejawnie przekształcić prostokątny¹ dwuwymiarową tablicę w jednowymiarową tablicę.

Co się dzieje, to to, że prostokątna tablica jest ułożona jako kolejne elementy w pamięci, więc może się zdegenerować do wskaźnika do elementów, a następnie parametr funkcji może zinterpretować to jako tablicę o innej geometrii.

Oto krótki mały program, który demonstruje to zachowanie.

#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>

#define ROWS 2
#define COLS 4
#define ELEMS (ROWS*COLS)

int flatten_array( const ptrdiff_t n, const int a[n] )
{
  int printed = 0;

  for ( ptrdiff_t i = 0; i < n; ++i )
    printed += printf( "%d ", a[i] );

  return printed + printf("\n");
}

int rectangular_array( const ptrdiff_t m,
                       const ptrdiff_t n,
                       const int a[m][n] )
{
  int printed = 0;

  for ( ptrdiff_t i = 0; i < m; ++i ) {
    for ( ptrdiff_t j = 0; j < n; ++j )
      printed += printf( "%d ", a[i][j] );

    printed += printf("\n");
  }

  return printed + printf("\n");
}

int main(void)
{
  static const int matrix[ROWS][COLS] = {
    {11, 12, 13, 14},
    {21, 22, 23, 24}
  };
  static const int vector[ELEMS] = {11, 12, 13, 14, 21, 22, 23, 24};

  flatten_array( ELEMS, *(const int (*const)[ELEMS])matrix );
  printf("\n");
  rectangular_array( ROWS, COLS, *(const int (*const)[ROWS][COLS])vector );

  return EXIT_SUCCESS;
}

W komentarzach poniżej znajduje się komentarz językowy dotyczący tego, czy przekazywanie argumentów tablicowych bez jawnych rzutów jest technicznie legalne w standardzie. Zdecydowałem się przenieść to do przypisu i po prostu usunąć przykład bez rzutowania. W realnym świecie czasami można zobaczyć kod bez rzutowania wskaźnika na tablicę o innej geometrii i może wygenerować ostrzeżenie. Układ pamięci obu tablic jest wymagany przez standard, aby był taki sam.

Aby przekonwertować do C ++, możesz użyć sztuczki do konwersji pointeru, lub możesz teraz zakodować trochę za pomocą referencji.

Oto tłumaczenie C ++ powyższego programu. Wymaga to tylko pierwszego wymiaru tablicy, która ma być przekazana constexpr, ale niektóre kompilatory obsługują tablice o zmiennej długości w stylu C99 jako rozszerzenie.

#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>

constexpr ptrdiff_t rows = 2;
constexpr ptrdiff_t cols = 4;
constexpr ptrdiff_t elems = rows * cols;

int flatten_array( const ptrdiff_t n, const int a[] )
{
  int printed = 0;

  for ( ptrdiff_t i = 0; i < n; ++i )
    printed += printf( "%d ", a[i] );

  return printed + printf("\n");
}

int rectangular_array( const ptrdiff_t n, const int a[][cols] )
{
  int printed = 0;

  for ( ptrdiff_t i = 0; i < n; ++i ) {
    for ( ptrdiff_t j = 0; j < cols; ++j )
      printed += printf( "%d ", a[i][j] );

    printed += printf("\n");
  }

  return printed + printf("\n");
}

int main(void)
{
  static const int matrix[rows][cols] = {
    {11, 12, 13, 14},
    {21, 22, 23, 24}
  };
  static const int vector[elems] = {11, 12, 13, 14, 21, 22, 23, 24};

  flatten_array( elems, (const int(&)[elems])matrix );
  printf("\n");
  rectangular_array( rows, (const int(&)[rows][cols])vector );

  return EXIT_SUCCESS;
}

¹ Programiści C czasami nazywają takie tablice jak int matrix[ROWS][COLS] lub tablice typu char** argv "Dwuwymiarowe tablice". Tu nazywam to pierwsze prostokątny i ten drugi obdarty.

² Ograniczenia dotyczące argumentów funkcji w standardzie C11 są następujące: "Każdy argument musi mieć taki typ, aby jego wartość mogła zostać przypisana do obiektu o niewykwalifikowanej wersji typu odpowiadającego mu parametru." Ponadto "Deklaracja parametru jako". "tablica typu" "jest dostosowywana do" kwalifikowanego wskaźnika do typu "" ", a jeśli to dotyczy rekursywnie, tablica wielowymiarowa jakiegoś typu zostanie dostosowana do płaskiego wskaźnika tego typu.


4
2018-02-22 04:19



rectangular_array( ROWS, COLS, vector ); jest naruszeniem ograniczenia dla podania niewłaściwego typu argumentu. To nie jest legalne. - M.M
@ M.M Zanim zdobędę dla ciebie cały język prawniczy: podaję rozwiązanie tego w mojej drugiej próbce kodu, więc użyj tego, jeśli wolisz. - Davislor
@ M.M Dodałem adnotację językową jako przypis. - Davislor


w odniesieniu do drugiej części pytania:

Dlaczego ta sama wersja nie działa w C ++? Kiedy dosłownie zmieniam rozszerzenie pliku z .c na .cpp i spróbuję przekompilować, dostaję

Źródłem tego problemu jest to, że C ++ miesza nazwy.

Aby uniknąć zniekształcania nazw podczas uruchamiania C ++ i próby uzyskania dostępu do biblioteki C.

w pobliżu górnej części pliku nagłówkowego biblioteki C po wstawieniu wielokrotnej osłony włączenia:

#ifdef __cplusplus
extern "C" {
#endif

i blisko końca pliku nagłówkowego, przed #endif wielokrotnego ochronnika wtrącania, wstaw:

#ifdef __cplusplus
}
#endif

To wyeliminuje problem funkcji, które znajdują się w pliku biblioteki skojarzonej


0
2018-02-22 09:12



Dla tych, którzy odrzucili tę odpowiedź, DLACZEGO ujęliście ją? - user3629249
Nie poszedłem w dół, ale to ma więcej wspólnego z VLA niż z manglingiem? - wbkang
oryginalny tekst pytania (kiedy wydaje się, że został drastycznie edytowany) pyta, dlaczego nie można znaleźć funkcji w bibliotece C, gdy funkcja wywołująca pochodzi z programu C ++. - user3629249
Cóż, to bardzo głupie. Myślę, że zdarza się to całkiem często. - wbkang
Pierwotne pytanie odnosi się do błędu kompilatora, a nie błędu linkera, więc nie widzę, w jaki sposób mangling nazw jest istotny dla tego pytania. A jeśli cały kod jest skompilowany jako C ++ (co jest implikowane przez pytanie - przełączenie typu pliku na .cpp), to mangling nazw nadal nie będzie problemem. - Steve Kidd