Inicjalizacja wszystkich elementów tablicy do jednej wartości domyślnej w C ++?

248

Uwagi na temat C ++: Inicjalizacja macierzy ma ładną listę nad inicjowaniem macierzy. mam

int array[100] = {-1};

spodziewając się, że będzie pełny z -1, ale nie, tylko pierwsza wartość jest, a reszta to 0 pomieszane z losowymi wartościami.

Kod

int array[100] = {0};

działa dobrze i ustawia każdy element na 0.

Czego tu brakuje? Nie można go zainicjować, jeśli wartość nie jest równa zero?

I 2: Czy domyślna inicjalizacja (jak wyżej) jest szybsza niż zwykła pętla przez całą tablicę i przypisuje wartość, czy robi to samo?

Mediolan
źródło
1
Zachowanie w C i C ++ jest inne. W C {0} jest szczególnym przypadkiem inicjatora struktury, jednak AFAIK nie dla tablic. int tablica [100] = {0} powinna być taka sama jak tablica [100] = {[0] = 0}, która jako efekt uboczny wyzeruje wszystkie pozostałe elementy. Kompilator AC NIE powinien zachowywać się tak, jak opisano powyżej, zamiast tego int array [100] = {- 1} powinien ustawić pierwszy element na -1, a resztę na 0 (bez szumu). W C, jeśli masz tablicę struct x [100], użycie = {0} jako inicjalizatora NIE jest poprawne. Możesz użyć {{0}}, który zainicjuje pierwszy element i wyzeruje wszystkie pozostałe, w większości przypadków będzie to samo.
Fredrik Widlund
1
@FredrikWidlund W obu językach jest tak samo. {0}nie jest specjalnym przypadkiem dla struktur ani tablic. Zasadą jest, że elementy bez inicjalizatora są inicjowane tak, jakby miały 0dla inicjatora. Jeśli istnieją zagnieżdżone agregaty (np. struct x array[100]), Wówczas inicjatory są stosowane do nieagregatów w kolejności „major-row”; nawiasy klamrowe można opcjonalnie pominąć. struct x array[100] = { 0 }jest ważny w C; i ważne w C ++, o ile pierwszy członek struct Xprzyjmuje 0jako inicjator.
MM
1
{ 0 }nie jest czymś specjalnym w C, ale o wiele trudniej jest zdefiniować typ danych, którego nie można zainicjować, ponieważ nie ma konstruktorów, a tym samym nie można powstrzymać się 0przed niejawną konwersją i przypisaniem do czegoś .
Leushenko
3
Głosowano na ponowne otwarcie, ponieważ drugie pytanie dotyczy C. Istnieje wiele sposobów C ++ na inicjalizację tablicy, które nie są prawidłowe w C.
xskxzr
1
Głosowano również na ponowne otwarcie - C i C ++ to różne języki
Pete

Odpowiedzi:

350

Używając użytej składni,

int array[100] = {-1};

mówi „ustaw pierwszy element na, -1a resztę na 0”, ponieważ wszystkie pominięte elementy są ustawione na 0.

W C ++, aby ustawić je wszystkie -1, możesz użyć czegoś takiego jak std::fill_n(z <algorithm>):

std::fill_n(array, 100, -1);

W przenośnym C musisz rozwinąć własną pętlę. Istnieją rozszerzenia kompilatora lub możesz polegać na zachowaniu zdefiniowanym jako implementacja jako skrót, jeśli jest to dopuszczalne.

Evan Teran
źródło
14
Odpowiadało to również na pytanie pośrednie dotyczące sposobu „łatwego” wypełnienia tablicy wartościami domyślnymi. Dziękuję Ci.
Milan
7
@chessofnerd: nie do końca, #include <algorithm>jest właściwym nagłówkiem, <vector>może, ale nie musi, zawierać go pośrednio, zależnie od twojej implementacji.
Evan Teran
2
Nie musisz uciekać się do inicjalizacji tablicy podczas działania. Jeśli naprawdę potrzebujesz inicjalizacji, aby nastąpiła statycznie, możesz użyć szablonów i sekwencji variadic do wygenerowania pożądanej sekwencji ints i rozwinięcia jej do inicjalizatora tablicy.
void-pointer
2
@ontherocks, nie, nie ma poprawnego sposobu użycia pojedynczego wywołania fill_ndo wypełnienia całej tablicy 2D. Musisz zapętlić jeden wymiar, wypełniając drugi.
Evan Teran
7
To jest odpowiedź na inne pytanie. std::fill_nnie jest inicjalizacją.
Ben Voigt
133

Istnieje rozszerzenie kompilatora gcc, które umożliwia składnię:

int array[100] = { [0 ... 99] = -1 };

Spowodowałoby to ustawienie wszystkich elementów na -1.

Nazywa się to „desygnowanymi inicjatorami” , aby uzyskać dodatkowe informacje.

Uwaga: nie jest to zaimplementowane w kompilatorze gcc c ++.

Callum
źródło
2
Niesamowite. Wydaje się, że ta składnia działa również w trybie clang (więc można jej używać w systemie iOS / Mac OS X).
JosephH
31

Strona, do której prowadziłeś link, już dawała odpowiedź na pierwszą część:

Jeśli określono jawny rozmiar tablicy, ale określono krótszą listę inicjującą, nieokreślone elementy są ustawiane na zero.

Nie ma wbudowanego sposobu na zainicjowanie całej tablicy na jakąś niezerową wartość.

Co do tego, co jest szybsze, stosuje się zwykłą zasadę: „Metoda, która daje kompilatorowi najwięcej swobody, jest prawdopodobnie szybsza”.

int array[100] = {0};

po prostu mówi kompilatorowi „ustaw te 100 int na zero”, które kompilator może dowolnie optymalizować.

for (int i = 0; i < 100; ++i){
  array[i] = 0;
}

jest o wiele bardziej szczegółowy. Nakazuje kompilatorowi utworzenie zmiennej iteracyjnej i, nakazuje kolejność inicjowania elementów i tak dalej. Oczywiście kompilator prawdopodobnie to zoptymalizuje, ale chodzi o to, że przesadnie określasz problem, zmuszając kompilator do cięższej pracy, aby uzyskać ten sam wynik.

Na koniec, jeśli chcesz ustawić tablicę na niezerową wartość, powinieneś (przynajmniej w C ++) użyć std::fill:

std::fill(array, array+100, 42); // sets every value in the array to 42

Ponownie, możesz zrobić to samo z tablicą, ale jest to bardziej zwięzłe i daje kompilatorowi więcej swobody. Mówisz tylko, że chcesz wypełnić całą tablicę wartością 42. Nie mów nic o tym, w jakiej kolejności należy to zrobić, ani nic innego.

jalf
źródło
5
Dobra odpowiedź. Zauważ, że w C ++ (nie w C) możesz wykonać int tablicę [100] = {}; i daj kompilatorowi najwięcej swobody :)
Johannes Schaub - litb
1
zgodził się, doskonała odpowiedź. Ale dla tablicy o stałym rozmiarze użyłby std :: fill_n :-P.
Evan Teran
12

C ++ 11 ma inną (niedoskonałą) opcję:

std::array<int, 100> a;
a.fill(-1);
Timmmm
źródło
lubstd::fill(begin(a), end(a), -1)
doctorlai
9

Za pomocą {} przypisujesz elementy tak, jak są zadeklarowane; reszta jest inicjowana na 0.

Jeśli nie = {}można zainicjować, treść jest niezdefiniowana.

0x6adb015
źródło
8

Strona, do której prowadziłeś połączenie, ma status

Jeśli określono jawny rozmiar tablicy, ale określono krótszą listę inicjującą, nieokreślone elementy są ustawiane na zero.

Problem prędkości: Wszelkie różnice byłyby znikome dla tak małych tablic. Jeśli pracujesz z dużymi tablicami, a szybkość jest o wiele ważniejsza niż rozmiar, możesz mieć stałą tablicę wartości domyślnych (zainicjowanych w czasie kompilacji), a następnie memcpymodyfikować tablicę.

laalto
źródło
2
memcpy nie jest zbyt dobrym pomysłem, ponieważ byłoby to porównywalne z ustawieniem wartości bezpośrednio pod względem prędkości.
Evan Teran
1
Nie widzę potrzeby kopiowania i stałej tablicy: dlaczego nie stworzyć w pierwszej kolejności modyfikowalnej tablicy z wstępnie wypełnionymi wartościami?
Johannes Schaub - litb
Dzięki za wyjaśnienie prędkości i jak to zrobić, jeśli prędkość jest problemem z dużym rozmiarem tablicy (co w moim przypadku)
Milan
Lista inicjalizująca jest wykonywana w czasie kompilacji i ładowana w czasie wykonywania. Nie trzeba kopiować.
Martin York
@litb, @Evan: Na przykład gcc generuje dynamiczną inicjalizację (wiele ruchów) nawet przy włączonych optymalizacjach. W przypadku dużych tablic i ścisłych wymagań wydajnościowych należy wykonać inicjalizację w czasie kompilacji. memcpy jest prawdopodobnie lepiej zoptymalizowany dla dużych kopii niż wiele zwykłych filmów.
laalto
4

Innym sposobem inicjalizacji tablicy do wspólnej wartości byłoby wygenerowanie listy elementów w szeregu definicji:

#define DUP1( X ) ( X )
#define DUP2( X ) DUP1( X ), ( X )
#define DUP3( X ) DUP2( X ), ( X )
#define DUP4( X ) DUP3( X ), ( X )
#define DUP5( X ) DUP4( X ), ( X )
.
.
#define DUP100( X ) DUP99( X ), ( X )

#define DUPx( X, N ) DUP##N( X )
#define DUP( X, N ) DUPx( X, N )

Inicjowanie tablicy do wspólnej wartości można łatwo wykonać:

#define LIST_MAX 6
static unsigned char List[ LIST_MAX ]= { DUP( 123, LIST_MAX ) };

Uwaga: Wprowadzono DUPx, aby umożliwić podstawienie makr w parametrach do DUP

Steen
źródło
3

W przypadku tablicy elementów jednobajtowych możesz użyć memset, aby ustawić wszystkie elementy na tę samą wartość.

Jest to przykład tutaj .

Steve Melnikoff
źródło
3

Używając tego std::array, możemy to zrobić w dość prosty sposób w C ++ 14. Można to zrobić tylko w C ++ 11, ale nieco bardziej skomplikowane.

Nasz interfejs jest czasem kompilacji i wartością domyślną.

template<typename T>
constexpr auto make_array_n(std::integral_constant<std::size_t, 0>, T &&) {
    return std::array<std::decay_t<T>, 0>{};
}

template<std::size_t size, typename T>
constexpr auto make_array_n(std::integral_constant<std::size_t, size>, T && value) {
    return detail::make_array_n_impl<size>(std::forward<T>(value), std::make_index_sequence<size - 1>{});
}


template<std::size_t size, typename T>
constexpr auto make_array_n(T && value) {
    return make_array_n(std::integral_constant<std::size_t, size>{}, std::forward<T>(value));
}

Trzecia funkcja służy głównie wygodzie, więc użytkownik nie musi sam konstruować std::integral_constant<std::size_t, size>, ponieważ jest to dość nieporęczna konstrukcja. Prawdziwa praca jest wykonywana przez jedną z dwóch pierwszych funkcji.

Pierwsze przeciążenie jest dość proste: konstruuje std::arrayrozmiar 0. Nie ma potrzeby kopiowania, po prostu go konstruujemy.

Drugie przeciążenie jest nieco trudniejsze. Przekazuje dalej wartość otrzymaną jako źródło, a także konstruuje instancję make_index_sequencei po prostu wywołuje inną funkcję implementacyjną. Jak wygląda ta funkcja?

namespace detail {

template<std::size_t size, typename T, std::size_t... indexes>
constexpr auto make_array_n_impl(T && value, std::index_sequence<indexes...>) {
    // Use the comma operator to expand the variadic pack
    // Move the last element in if possible. Order of evaluation is well-defined
    // for aggregate initialization, so there is no risk of copy-after-move
    return std::array<std::decay_t<T>, size>{ (static_cast<void>(indexes), value)..., std::forward<T>(value) };
}

}   // namespace detail

To konstruuje argumenty pierwszego rozmiaru - 1, kopiując przekazaną przez nas wartość. W tym przypadku wykorzystujemy nasze różnorodne indeksy pakietów parametrów jako coś do rozszerzenia. W tej paczce są wpisy wielkości - 1 (jak określono w konstrukcji make_index_sequence) i mają one wartości 0, 1, 2, 3, ..., rozmiar - 2. Jednak nie dbamy o wartości ( więc odrzucamy go, aby wyciszyć wszelkie ostrzeżenia kompilatora). Rozszerzenie pakietu parametrów rozszerza nasz kod do czegoś takiego (przy założeniu rozmiaru == 4):

return std::array<std::decay_t<T>, 4>{ (static_cast<void>(0), value), (static_cast<void>(1), value), (static_cast<void>(2), value), std::forward<T>(value) };

Używamy tych nawiasów, aby zapewnić, że rozszerzenie pakietu variadic ...rozszerza to, czego chcemy, a także aby upewnić się, że używamy operatora przecinka. Bez nawiasów wyglądałoby na to, że przekazujemy kilka argumentów do inicjalizacji tablicy, ale tak naprawdę oceniamy indeks, rzutujemy go na void, ignorujemy ten wynik void, a następnie zwracamy wartość, która jest kopiowana do tablicy .

Ostatni argument, ten, który wzywamy std::forward, to drobna optymalizacja. Jeśli ktoś przekaże tymczasowe std :: string i powie „zrób tablicę 5 z nich”, chcielibyśmy mieć 4 kopie i 1 ruch zamiast 5 kopii. W std::forwardgwarantuje, że możemy to zrobić.

Pełny kod, w tym nagłówki i niektóre testy jednostkowe:

#include <array>
#include <type_traits>
#include <utility>

namespace detail {

template<std::size_t size, typename T, std::size_t... indexes>
constexpr auto make_array_n_impl(T && value, std::index_sequence<indexes...>) {
    // Use the comma operator to expand the variadic pack
    // Move the last element in if possible. Order of evaluation is well-defined
    // for aggregate initialization, so there is no risk of copy-after-move
    return std::array<std::decay_t<T>, size>{ (static_cast<void>(indexes), value)..., std::forward<T>(value) };
}

}   // namespace detail

template<typename T>
constexpr auto make_array_n(std::integral_constant<std::size_t, 0>, T &&) {
    return std::array<std::decay_t<T>, 0>{};
}

template<std::size_t size, typename T>
constexpr auto make_array_n(std::integral_constant<std::size_t, size>, T && value) {
    return detail::make_array_n_impl<size>(std::forward<T>(value), std::make_index_sequence<size - 1>{});
}

template<std::size_t size, typename T>
constexpr auto make_array_n(T && value) {
    return make_array_n(std::integral_constant<std::size_t, size>{}, std::forward<T>(value));
}



struct non_copyable {
    constexpr non_copyable() = default;
    constexpr non_copyable(non_copyable const &) = delete;
    constexpr non_copyable(non_copyable &&) = default;
};

int main() {
    constexpr auto array_n = make_array_n<6>(5);
    static_assert(std::is_same<std::decay_t<decltype(array_n)>::value_type, int>::value, "Incorrect type from make_array_n.");
    static_assert(array_n.size() == 6, "Incorrect size from make_array_n.");
    static_assert(array_n[3] == 5, "Incorrect values from make_array_n.");

    constexpr auto array_non_copyable = make_array_n<1>(non_copyable{});
    static_assert(array_non_copyable.size() == 1, "Incorrect array size of 1 for move-only types.");

    constexpr auto array_empty = make_array_n<0>(2);
    static_assert(array_empty.empty(), "Incorrect array size for empty array.");

    constexpr auto array_non_copyable_empty = make_array_n<0>(non_copyable{});
    static_assert(array_non_copyable_empty.empty(), "Incorrect array size for empty array of move-only.");
}
David Stone
źródło
Twój non_copyabletyp jest w rzeczywistości możliwy do skopiowania za pomocą operator=.
Hertz
Przypuszczam non_copy_constructible, że byłaby to dokładniejsza nazwa obiektu. Jednak nigdzie w tym kodzie nie ma przypisania, więc nie ma to znaczenia w tym przykładzie.
David Stone
1

1) Gdy używasz inicjalizatora, dla struktury lub takiej tablicy, nieokreślone wartości są zasadniczo konstruowane domyślnie. W przypadku typu pierwotnego, takiego jak ints, oznacza to, że zostaną wyzerowane. Zauważ, że dotyczy to rekurencyjnie: możesz mieć tablicę struktur zawierających tablice, a jeśli podasz tylko pierwsze pole pierwszej struktury, wówczas cała reszta zostanie zainicjowana zerami i domyślnymi konstruktorami.

2) Kompilator prawdopodobnie wygeneruje kod inicjujący, który jest co najmniej tak dobry, jak można to zrobić ręcznie. W miarę możliwości wolę pozwolić kompilatorowi wykonać inicjalizację.

Boojum
źródło
1) Nie ma tutaj domyślnej inicjalizacji POD. Korzystając z listy, kompilator wygeneruje wartości w czasie kompilacji i umieści je w specjalnej sekcji zestawu, która jest właśnie ładowana w ramach inicjalizacji programu (np. Kod). Koszt jest więc zerowy w czasie wykonywania.
Martin York
1
Nie rozumiem, gdzie się myli? int a [100] = {} z pewnością jest inicjowany na wszystkie 0, niezależnie od tego, gdzie się pojawia, i struct {int a; } b [100] = {}; jest też. „zasadniczo skonstruowana domyślnie” => „wartość skonstruowana”, tys. Ale to nie ma znaczenia w przypadku ints, PODS lub typów z zadeklarowanymi przez użytkownika lekarzami. To ma znaczenie tylko dla NON-Pods bez zadeklarowanych przez użytkownika lekarzy, co wiem. Ale z tego powodu nie oddałbym (!) Głosu. tak czy inaczej, +1 dla ciebie, abyś znów miał 0 :)
Johannes Schaub - litb
@Evan: Zakwalifikowałem moje stwierdzenie słowami „Kiedy używasz inicjalizatora ...” Nie miałem na myśli niezainicjowanych wartości. @Martin: Może to działać dla danych stałych, statycznych lub globalnych. Ale nie rozumiem, jak to by działało z czymś takim jak: int test () {int i [10] = {0}; int v = i [0]; i [0] = 5; return v; } Kompilator powinien lepiej inicjować od i [] do zer za każdym razem, gdy wywołujesz test ().
Boojum
może umieszczać dane w segmencie danych statycznych i sprawiać, by odnosiły się do niego „i” :)
Johannes Schaub - litb
To prawda - technicznie w tym przypadku może również całkowicie pominąć „i” i po prostu zwrócić 0. Jednak użycie statycznego segmentu danych dla danych zmiennych może być niebezpieczne w środowiskach wielowątkowych. W odpowiedzi na pytanie Martina chciałem po prostu powiedzieć, że nie można całkowicie wyeliminować kosztów inicjalizacji. Skopiuj wstępnie utworzony fragment ze statycznego segmentu danych, jasne, ale nadal nie jest wolny.
Boojum
0

W języku programowania C ++ V4 Stroustrup zaleca używanie wektorów lub wartości zamiast wbudowanych tablic. Dzięki valarrary's, kiedy je utworzysz, możesz zainicjować je do określonej wartości, takiej jak:

valarray <int>seven7s=(7777777,7);

Aby zainicjować tablicę 7 członków o długości „7777777”.

Jest to sposób implementacji odpowiedzi w C ++ przy użyciu struktury danych C ++ zamiast „zwykłej starej tablicy C”.

Przerzuciłem się na używanie valarray jako próby w moim kodzie, aby spróbować użyć C ++ 'isms v. C'isms ....

Astara
źródło
To drugi najgorszy przykład tego, jak używać takiego typu, jaki kiedykolwiek widziałem ...
Steazy
-3

Powinien być standardową funkcją, ale z jakiegoś powodu nie jest uwzględniony w standardowym C ani C ++ ...

#include <stdio.h>

 __asm__
 (
"    .global _arr;      "
"    .section .data;    "
"_arr: .fill 100, 1, 2; "
 );

extern char arr[];

int main() 
{
    int i;

    for(i = 0; i < 100; ++i) {
        printf("arr[%u] = %u.\n", i, arr[i]);
    }
}

W Fortran można wykonać:

program main
    implicit none

    byte a(100)
    data a /100*2/
    integer i

    do i = 0, 100
        print *, a(i)
    end do
end

ale nie ma niepodpisanych liczb ...

Dlaczego C / C ++ nie może tego po prostu zaimplementować? Czy to naprawdę takie trudne? To głupie, że trzeba to napisać ręcznie, aby osiągnąć ten sam wynik ...

#include <stdio.h>
#include <stdint.h>

/* did I count it correctly? I'm not quite sure. */
uint8_t arr = {
    2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
    2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
    2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
    2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
    2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
    2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
    2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
    2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
    2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
    2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
};    

int main() 
{
    int i;

    for(i = 0; i < 100; ++i) {
        printf("arr[%u] = %u.\n", i, arr[i]);
    }
}

Co jeśli byłby to tablica o wielkości 1000,00 bajtów? Musiałbym napisać skrypt, aby go napisać dla mnie, lub uciekać się do hacków z asemblerem / etc. To nonsens.

Jest doskonale przenośny, nie ma powodu, aby nie być w języku.

Wystarczy zhakować to w następujący sposób:

#include <stdio.h>
#include <stdint.h>

/* a byte array of 100 twos declared at compile time. */
uint8_t twos[] = {100:2};

int main()
{
    uint_fast32_t i;
    for (i = 0; i < 100; ++i) {
        printf("twos[%u] = %u.\n", i, twos[i]);
    }

    return 0;
}

Jednym ze sposobów włamania się do niego jest wstępne przetwarzanie ... (Poniższy kod nie obejmuje przypadków skrajnych, ale został napisany, aby szybko zademonstrować, co można zrobić).

#!/usr/bin/perl
use warnings;
use strict;

open my $inf, "<main.c";
open my $ouf, ">out.c";

my @lines = <$inf>;

foreach my $line (@lines) {
    if ($line =~ m/({(\d+):(\d+)})/) {
        printf ("$1, $2, $3");        
        my $lnew = "{" . "$3, "x($2 - 1) . $3 . "}";
        $line =~ s/{(\d+:\d+)}/$lnew/;
        printf $ouf $line;
    } else {
        printf $ouf $line;
    }
}

close($ouf);
close($inf);
Dmitry
źródło
drukujesz w pętli, dlaczego nie możesz przypisać w pętli?
Abhinav Gauniyal
1
przypisanie wewnątrz pętli powoduje narzut związany z czasem działania; podczas gdy stałe kodowanie bufora jest darmowe, ponieważ bufor jest już osadzony w pliku binarnym, więc nie marnuje czasu na tworzenie tablicy od zera przy każdym uruchomieniu programu. masz rację, że drukowanie w pętli nie jest ogólnie dobrym pomysłem, lepiej dołączyć wewnątrz pętli, a następnie wydrukować jeden raz, ponieważ każde wywołanie printf wymaga wywołania systemowego, podczas gdy konkatenacja ciągów przy użyciu sterty / stosu aplikacji nie. Ponieważ rozmiar tego rodzaju programu nie jest problemem, najlepiej jest zbudować tę tablicę w czasie kompilacji, a nie w czasie wykonywania.
Dmitry
„przypisywanie w pętli powoduje narzut związany z czasem pracy” - poważnie nie doceniasz optymalizatora.
Asu
W zależności od rozmiaru tablicy, gcc i clang będą „kodować na twardo” lub oszukiwać wartość, a przy większych tablicach bezpośrednio memsetto, nawet z tablicą „na stałe”.
Asu