Automatyczne wykrywanie typu

2

W C# od zawsze, a w C++ od jakiegoś czasu możliwa jest deklaracja zmiennej bez podawania typu w przypadku, gdy można jednoznacznie wykryć, jaki typ powinna mieć ta zmienna. Do tego służy słowo var i auto.

Moim zdaniem to jest bardziej mało użyteczny bajer niż bardzo pożądana funkcjonalność. Jedyną zaletą, jaką widzę, jest to, że nie trzeba zmieniać definicji zmiennej, jak nagle zajdzie potrzeba zmiany typu zwracanej wartości funkcji, która nadaje tej zmiennej wartość (właśnie po tym kompilator dedukuje, jaki tym ma mieć ta zmienna), jednakże takie przypadki są bardzo sporadyczne.

Zauważyłem, że automatyczne wykrywanie typu zmiennej stało się bardzo popularne w jezykach z sztywnymi typami zmiennych wyposażonymi w taką możliwość.

Jednak dostrzegłem olbrzymią wadę, która dla mnie przesłania opisaną wyżej korzyść. Weźmy przykład z życia wzięty. Niedawno potrzebowałem zrobić coś takiego, że program rozpoczyna odmierzanie czasu i wykonuje jakieś obliczenia, potem program wykonuje dalszy kod, albo po wykonaniu obliczeń, albo minimum 3 sekundy od momentu rozpoczęcia obliczeń. W C# do tego celu może służyć System.Diagnostic.Stopwatch, potrzebuję tego, ale w C++.

No to wpisuję w Google c++ stopwatch i na jednym z pierwszych miejsc jest ta strona: https://www.pluralsight.com/blog/software-development/how-to-measure-execution-time-intervals-in-c-- . To jest praktycznie to, czego szukam, tylko trochę przerobić i zrezygnować z przeliczania na liczbę sekund (wystarczy zliczać wewnętrzne jednostki). Jak widać, mamy auto start = std::chrono::high_resolution_clock::now();. Chcę zobaczyć, jakie ma metody obiekt start, Qt Creator nie chce podpowiadać, bo nie wie, jakiego typu jest start, to się okaże przy kompilacji. Przechodzę do definicji metody now() i trafiam do skomplikowanego kodu z biblioteki, w której też nie widać, jakiego typu wartość zwraca ta funkcja.

Nie poddaję się, wpisuje w Google std::chrono::high_resolution_clock::now() i trafiam na https://en.cppreference.com/w/cpp/chrono/high_resolution_clock/now i właściwie nadal nie wiem, czym jest zmienna start. Dopiero z https://www.cplusplus.com/reference/chrono/high_resolution_clock/now/ dowiaduję się, że jest to std::chrono::high_resolution_clock::time_point.

Dopiero, jak w kodzie zmieniłem auto na std::chrono::high_resolution_clock::time_point to IDE już sam umiał podpowiedzieć funkcje i do tego wiedziałem o czym czytać, jakbym chciał z tą zmienną dalej coś robić.

Inny przykład. Jakiś czas temu, w C# potrzebowałem przejrzeć slownik typu System.Collection.Dictionary<int,string>. Wpisuje w google coś, co kieruje mnie do kodu tego pokroju:

Dictionary<int, string> X;
foreach (var item in X)
{
}

Kod się kompiluje, ale Visual Studio nie umie podpowiedzieć, czym jest item ani co on zawiera. Dopiero, jak po kilku dalszych poszukiwaniach i próbach doczytałem, ze jest to KeyValuePair<inst,string>, to problem się rozwiązał i kompilator podpowiedział, że item ma właściwości key i value. Nieraz miałem do czynienia z innymi podobnymi obiektami, które też mają klucz i wartość, jednak innego typu i problem się powtarzał.

Co przesądza o tym, że programiści tak chętnie korzystają z auto i var mimo takich niedogodności z tym związanych? Argument, że słowo auto jest krótsze od słowa std::chrono::high_resolution_clock::time_point nie przemawia do mnie. A jeżeli nie wiadomo, jaki będzie docelowy typ i możliwe, że trzeba zmienić, albo nazwa typu jest za dluga, to przecież w C++ jest typedef, które służy do takich celów, ewentualnie też jest #define.

6

Totalnie się nie zgadzam, ale doceniam post, bo może wyjść z niego ciekawa dyskusja imho.

Jakbyś musiał więcej poużywać iteratorów to byś docenił zwięzłość ;). Poza tym w C++ dochodzi jeszcze temat forwarding references uzyskiwanych przez auto&&. Last but not least - podmieniasz kontener/argument w jednym miejscu, auto wymusi prawidłowy typ wszędzie dalej. Łatwiej po prostu utrzymywać kod.

0

Jest jeszcze jedno, doprawdy chcesz pisać: std::chrono::high_resolution_clock::time_point zamiast auto?
Wiesz że czasami ten typ może przekraczać 200 znaków (miałem przypadek, prawie 250) i to zamiast 4 znaków auto?
Słyszałeś coś o czytelności kodu?

Co do

foreach (var item in X)
{
}

to wpisujesz wewnątrz pętli item. i trochę poczekać, dostaniesz podpowiedź jakie masz składowe do wyboru i jakiego typu
zaś jak wpiszesz samo i to z dużym prawdopodobieństwem dostaniesz podpowiedź item z padaniem typu.

1

Rozważ zmianę IDE na takie które podpowiada, bądź sprawdź czy da się tak skonfigurować.
VSCode sobie radzi z std::chrono::high_resolution_clock::now() gdy jest auto.
screenshot-20210528223351.png
Co do używania, to popatrz na ten screen, pisałbyś taki typ za każdym razem? Pomaga i poprawia czytelność przy pracy z iteratorami(np zmiana kontenera na inny minimalnym nakładem pracy gdy się iteratory pokrywają), unikasz błędów z dedukcją ręczną zwłaszcza gdy pracujesz z szablonami itp.

0
_13th_Dragon napisał(a):

Jest jeszcze jedno, doprawdy chcesz pisać: std::chrono::high_resolution_clock::time_point zamiast auto?

Wiesz że czasami ten typ może przekraczać 200 znaków (miałem przypadek, prawie 250) i to zamiast 4 znaków auto?
Słyszałeś coś o czytelności kodu?

Tutaj masz rację, ale:

  1. Jak już wcześniej napisałem, jeżeli długość typu przeszkadza i czyni kod nieczytelny, to sprawę rozwiązuje typedef std::chrono::high_resolution_clock::time_point lib_time_point.
  2. Załóżmy, że jest to zmienna typu int lub long. Jeżeli w jakimś miejscu jest użyta lub chce użyć, a nie pamiętam dobrze typu (a wcześniej nie miało to znaczenia, teraz ma istotne znaczenie), jak raz wciskam F2 lub F12 (w zależności od IDE) i już widzę zmienną z typem obok (lub z zamiennikiem zdefiniowanym za pomocą typedef). A jak jest auto, to muszę patrzeć dalej. Można oczywiście samą zmienną tak nazwać, że sugeruje ona typ, ale to już inny temat.
DiabolicalOnion napisał(a):

Rozważ zmianę IDE na takie które podpowiada, bądź sprawdź czy da się tak skonfigurować.

VSCode sobie radzi z std::chrono::high_resolution_clock::now() gdy jest auto.
screenshot-20210528223351.png
Co do używania, to popatrz na ten screen, pisałbyś taki typ za każdym razem? Pomaga i poprawia czytelność przy pracy z iteratorami(np zmiana kontenera na inny minimalnym nakładem pracy gdy się iteratory pokrywają), unikasz błędów z dedukcją ręczną zwłaszcza gdy pracujesz z szablonami itp.

Zgadzam się, że przedstawiony kod jest nieczytelny. Ale skoro używa się w wielu miejscach biblioteki chrono, to na początku stawia się using namespace std::chrono i wyrzuca się ten prefiks z każdej definicji typu, już robi się przejrzyście i widać co jest czym.

0
_13th_Dragon napisał(a):

Co do

foreach (var item in X)
{
}

to wpisujesz wewnątrz pętli item. i trochę poczekać, dostaniesz podpowiedź jakie masz składowe do wyboru i jakiego typu
zaś jak wpiszesz samo i to z dużym prawdopodobieństwem dostaniesz podpowiedź item z padaniem typu.

Właśnie tak robiłem w Visual Studio 2008 lub 2012 (nie pamiętam, jaki wtedy miałem) i nie podpowiedział, albo podpowiedział za mało. To było dawno, ale podałem dla przykładu analogicznego problemu w C#. Może najnowszy Visual Studio, czy VSCode, czy inny IDE do C# jest lepiej zrobiony.

Zróbmy eksperyment:

Dictionary<int, string> item
// Tu jakaś bardzo duża ilość kodu lub inny plik
foreach (var item in X)
{
}
Dictionary<int, string> item
// Tu jakaś bardzo duża ilość kodu lub inny plik
foreach (KeyValuePair<int, string> item in X)
{
}

W którym przypadku będąc przy pętli (i nie widzisz pierwszej linii) na pierwszy rzut oka widzisz, czym jest item, co przechowuje i co możesz z nim zrobić?

Oczywiście ja nie twierdzę, że auto to zło, tylko szukałem argumentów za używaniem i czy da się obalić moje argumenty przeciw. Widzę, że już jedno i drugie jest napisane.

1

Moim zdaniem użyteczność takiego var jest bardzo niska, a totalną farsą jest kiedy ludzie którzy tego używają, jednocześnie mają w IDE włączone pokazywanie faktycznego typu (IntelliJ robi tak domyślnie).
W efekcie masz w kodzie takie coś:

screenshot-20210528230334.png

I potem ktoś się podnieca że napisał var a jednocześnie polega na tym, ze IDE podpowie mu co to za typ. Równie dobrze można było zrobić extract-variable i IDE wygenerowałoby przypisanie z odpowiednim typem. Nie wiem, może w C++ jest to jakoś mocno przydatniejsze bo jakieś chore zagnieżdżone namespace, ale trochę wątpie.

0

A jeżeli nie wiadomo, jaki będzie docelowy typ i możliwe, że trzeba zmienić, albo nazwa typu jest za dluga, to przecież w C++ jest typedef, które służy do takich celów, ewentualnie też jest #define.

No ale czasem nie wiadomo jaki będzie typ, w znaczeniu, nigdy nie będzie wiadomo, np.

std::visit(overloaded {
   [] (int x) { 
         // coś dla inta
   }, [] (double d) {
         // coś dla doubla
   }, [] (auto a) {
          // przypadek domyślny, dla każdego innego typu z wariantu
   }
}, v);

Czym innym jest leniwość w postaci takiego auto do iteracji po słowniku, tam bym faktycznie odradzał. Z drugiej strony, już przy ręcznym użyciu iteratorów, jak najbardziej widzę użycie auto, szczególnie gdy mamy do czynienia z niejasnymi szablonami.

@_13th_Dragon:
250 znaków to dużo jak na nazwę typu? U mnie były przykłady (z szablonami), gdzie prawdziwa nazwa miała ponad 10 tysięcy.

7
andrzejlisek napisał(a):

Właśnie tak robiłem w Visual Studio 2008 lub 2012 (nie pamiętam, jaki wtedy miałem) i nie podpowiedział, albo podpowiedział za mało. To było dawno, ale podałem dla przykładu analogicznego problemu w C#. Może najnowszy Visual Studio, czy VSCode, czy inny IDE do C# jest lepiej zrobiony.

Dokładnie tak. Od tamtego czasu technologia poszła do przodu i IDE wysypuje podpowiedzi w zasadzie od razu, tak samo jak podaje typ zmiennej po nakierowaniu kursora. Co do pytania "po co" to odpowiedź jest prosta- dla lepszej czytelności kodu. Programiści większość czasu czytają kod, a większość tej większości czasu to czytanie bez zagłębiania się w szczegóły. Chodzi o skupienie się na procesie/logice, co przy zwięzłym kodzie i odpowiednio nazwanych zmiennych znacznie ułatwia życie. Twój przykład właśnie przedstawia tego odwrotność, bo jest różnica między poniższymi:

Dictionary<int, Person> X;
foreach (var item in X)
{
  Process(item);
  //...
}

// vs

Dictionary<int, Person> peopleLookup;
foreach (var person in peopleLookup)
{
  SendEmailNotification(person.EmailAddress);
  //...
}

To się może wydawać trywialne, ale w szerszej perspektywie ma duże znaczenie.

5

Nie poddaję się, wpisuje w Google std::chrono::high_resolution_clock::now() i trafiam na https://en.cppreference.com/w/cpp/chrono/high_resolution_clock/now i właściwie nadal nie wiem, czym jest zmienna start. Dopiero z https://www.cplusplus.com/reference/chrono/high_resolution_clock/now/ dowiaduję się, że jest to std::chrono::high_resolution_clock::time_point.

Jakim cudem, skoro na https://en.cppreference.com/w/cpp/chrono/high_resolution_clock/now w pierwszej linijce masz całą sygnaturę

static std::chrono::time_point<std::chrono::high_resolution_clock> now() noexcept; (since C++11)

Co więcej, ten typ zwracany jest też linkiem, który można kliknąć?

0
Spearhead napisał(a):

Nie poddaję się, wpisuje w Google std::chrono::high_resolution_clock::now() i trafiam na https://en.cppreference.com/w/cpp/chrono/high_resolution_clock/now i właściwie nadal nie wiem, czym jest zmienna start. Dopiero z https://www.cplusplus.com/reference/chrono/high_resolution_clock/now/ dowiaduję się, że jest to std::chrono::high_resolution_clock::time_point.

Jakim cudem, skoro na https://en.cppreference.com/w/cpp/chrono/high_resolution_clock/now w pierwszej linijce masz całą sygnaturę

static std::chrono::time_point<std::chrono::high_resolution_clock> now() noexcept; (since C++11)

Co więcej, ten typ zwracany jest też linkiem, który można kliknąć?

Możliwe, że trafiłem na inną stronę, faktycznie jest tam odpowiednia informacja. Nie chodzi tu o analizę tego konkretnego przypadku ani o to, za którym razem uzyskałem pełną informację, tylko o to, że szukam i niekiedy muszę później dedukować (poprzez dalsze szukanie, czytanie), jakiego typu informacje zwraca lub przyjmuje jakaś metoda.

0

Pytanie brzmi, po kiego szukasz?
Z ciekawości - no to twój problem.
Bo nie wiesz co z tym robić?

  • Jeżeli twój kod to napisz nazwę tej zmiennej var zaś IDE ci podpowie jakie ma metody składowe (ew. właściwości).
  • Jeżeli nie twój kod to z napisanego dalej kodu powinno wynikać co z tym autor robi.
6

Uważam, że podejście opisywane w 1. poście jest równie słabe jak Almost Always Auto.

Pomijając sytuacje, gdzie nie ma wyboru (np. lambdy), pierwszą rzeczą pod którą optymalizujemy jest czytelność kodu. Potem jego modyfikowalność. Czasem auto znakomicie zwiększa czytelność, czasem powoduje, że kod jest nieczytelny

Np:

auto it = last_accessed.find(param);
if (it != last_accessed.cend()) {
    fmt::print("Last accessed: {}", it->second);
else {
    fmt::print("Not found");
}

lub

if (auto it = last_accessed.find(param); it != last_accessed.cend()) {
    fmt::print("Last accessed: {}", it->second);
else {
    fmt::print("Not found");
}

vs

std::map<std::string, std::chrono::time_point<std::chrono::high_resolution_clock>>::const_iterator it = last_accessed.find(param);
if (it != last_accessed.cend()) {
    fmt::print("Not found");
else {
    fmt::print("Last accessed: {}", it->second);
}

A to nie jest nawet jakiś mega ekstremalny przypadek.

Z drugiej strony, są przypadki takie jak w dokumentacji D:

auto redBlackTree(E)(E[] elems...);

auto redBlackTree(bool allowDuplicates, E)(E[] elems...);

auto redBlackTree(alias less, E)(E[] elems...)
if (is(typeof(binaryFun!less(E.init, E.init))));

auto redBlackTree(alias less, bool allowDuplicates, E)(E[] elems...)
if (is(typeof(binaryFun!less(E.init, E.init))));

auto redBlackTree(Stuff)(Stuff range)
if (isInputRange!Stuff && !isArray!Stuff);

auto redBlackTree(bool allowDuplicates, Stuff)(Stuff range)
if (isInputRange!Stuff && !isArray!Stuff);

auto redBlackTree(alias less, Stuff)(Stuff range)
if (is(typeof(binaryFun!less((ElementType!Stuff).init, (ElementType!Stuff).init))) && isInputRange!Stuff && !isArray!Stuff);

auto redBlackTree(alias less, bool allowDuplicates, Stuff)(Stuff range)
if (is(typeof(binaryFun!less((ElementType!Stuff).init, (ElementType!Stuff).init))) && isInputRange!Stuff && !isArray!Stuff);
import std.range : iota;

auto rbt1 = redBlackTree(0, 1, 5, 7);
auto rbt2 = redBlackTree!string("hello", "world");
auto rbt3 = redBlackTree!true(0, 1, 5, 7, 5);
auto rbt4 = redBlackTree!"a > b"(0, 1, 5, 7);
auto rbt5 = redBlackTree!("a > b", true)(0.1, 1.3, 5.9, 7.2, 5.9);

// also works with ranges
auto rbt6 = redBlackTree(iota(3));
auto rbt7 = redBlackTree!true(iota(3));
auto rbt8 = redBlackTree!"a > b"(iota(3));
auto rbt9 = redBlackTree!("a > b", true)(iota(3));

W tym przypadku auto uważam za szkodliwe i czyniące dokumentację znacznie mniej użyteczną.

Jeśli chodzi o zgodność z IDE: to jest problem IDE, to nie Java czy C# żeby IDE dyktowało styl kodowania. Poza tym, od dawna nie miałem problemów zarówno w Qt Creatorze jak i w VS Code.

0

@kq mnie uprzedził :D
Generalnie nikt nie broni typu deklarować explicite kiedy jest ważny, ale jak przelatujesz po kontenerze to z reguły jest to szczegół implementacyjny.

3

OIDP jednym z oficjalnych zastosowań auto jest nowy sposób deklaracji zmiennych, w którym nazwa jest zawsze po lewej, tak by powoli pozbywać się uciążliwych aspektów "C heritage".
Czyli auto a = MyCLass{1234}; zamiast MyClass a(1234);
Ma to zapobiegać błędom typu:

mutex m;
lock_guard<std::mutex>(m);

Innym zastosowaniem jest współpraca z templates, gdzie inaczej dostałbyś pier*olca a nie poprawnie zadeklarował iterator.
Szczególnie widoczne jest to przy bibliotece chrono, która operuje prawie wyłącznie na templatach jak np. std::chrono::time_point. Którego typ nie jest znany chyba nikomu aż do momentu kompilacji, kiedy dopiero zostanie wygenerowany w sposób optymalny dla zastosowania.

Jeśli chodzi o IDE, to większość jest za głupia by zrozumieć templaty (czyli generację kodu w momencie kompilacji). IDE zostały stworzone z myślą o trywialnych językach.

2

I potem ktoś się podnieca że napisał var a jednocześnie polega na tym, ze IDE podpowie mu co to za typ. Równie dobrze można było zrobić extract-variable i IDE wygenerowałoby przypisanie z odpowiednim typem.

No tak, mógłbym powiedzieć że po co recordy jak moge w Javie z IntelliJ wygenerować konstruktor i gettery. Zaletą var/val/auto etc ma być zmniejszenie ilości kodu który się pisze. No ja tam wolę pisać

var albums = Map.of("Affinity", 2016, "Virus", 2020)

niż

Map<String, Integer> albums = Map.of("Affinity", 2016, "Virus", 2020)

Jakby nie nick to pomyślałbym że to post jakiegość "seniora" ktory równie dobrze by mógł napisac że pętle i ify sa lepsze od Streamów.

2

Na marginesie -- ciekawe, że Rust potrafi automatycznie wnioskować typy, ale mimo to nie pozwala ich opuszczać w sygnaturach funkcji (nazwanych). Właśnie ze względu na czytelność i porządek (żeby przez przypadek nie zrobić funkcji, która operuje nie na tym, na czym miała...

0
kq napisał(a):

Uważam, że podejście opisywane w 1. poście jest równie słabe jak Almost Always Auto.

Och, a dlaczego uważasz AAA za słabe? Właśnie miałem zacytować ten artykuł jako jeden z moich ulubionych. Przypadek który podałeś z języka D nie wygląda jakby się odnosił do tego artykułu.

0
koszalek-opalek napisał(a):

Na marginesie -- ciekawe, że Rust potrafi automatycznie wnioskować typy, ale mimo to nie pozwala ich opuszczać w sygnaturach funkcji (nazwanych).

W sensie porownujesz to do pelnego polimorfizmu parametrycznego, ktory mozna zobaczyc w np. Haskellu? Chociaz w Haskellu i tak sie przyjelo typy podawac, wiec moze OCaml to lepszy przyklad.

Właśnie ze względu na czytelność i porządek (żeby przez przypadek nie zrobić funkcji, która operuje nie na tym, na czym miała...

Ale wtedy np. wywolanie foo(bar) gdzie typ bar sie nie zgadza byloby nielegalne i by sie nie kompilowalo.

0

Moim zdaniem to jest bardziej mało użyteczny bajer niż bardzo pożądana funkcjonalność.

Czy taki mało użyteczny to nie wiem, zamiast pisać coś takiego:

std::unordered_map<std::string, std::vector<double>>::iterator

a zdarzały się dłuższe przypadki - to piszę auto i siema.

0
kmle napisał(a):
kq napisał(a):

Uważam, że podejście opisywane w 1. poście jest równie słabe jak Almost Always Auto.

Och, a dlaczego uważasz AAA za słabe? Właśnie miałem zacytować ten artykuł jako jeden z moich ulubionych. Przypadek który podałeś z języka D nie wygląda jakby się odnosił do tego artykułu.

Przypadek z dokumentacji odnosi się do auto/dedukcji typów w ogóle. Propozycja Suttera (zawsze używaj auto gdy możesz) imo obniża czytelność kodu, co jest najważniejsze. Ten post dobrze opisuje moje podejście do auto - szukałem innego, ale znalazłem ten.

Swoją drogą, od C++20 zamiast "głupiego" auto można podawać koncepty, co zapewne będzie gwoździem do trumny AAA:

template<typename T>
concept HasSize = requires(T obj)
{
    obj.size();    
};

int main()
{
    HasSize auto const& a = "foo"s;
    HasSize auto const& b = vector{1,2,3};
    // HasSize auto const& c = 5; // error
}

https://wandbox.org/permlink/RTmPYNcjRebEMjZU

3

@Aleksander32: > . Zaletą var/val/auto etc ma być zmniejszenie ilości kodu który się pisze.

Prawda, ale prawdziwy cel var/val/auto czy generalnie wnioskowania typów to zwiększenie(!!!) możliwości statycznej kontroli typów.
Im mniej musisz pisać tym bardziej dokładnych typów możesz wygodnie używać. Dobrze to widać w Scali i Haskellu - trochę też w Kotlinie (aczkolwiek tam system typów jest biedny prawie jak w javie).

Co do oryginalnego posta.
Należy odróżnić problem języka i kompilatora (którego to problemu raczej tu nie widze) od problemu konkretnego edytora/IDE, który jest po prostu biedny.

1 użytkowników online, w tym zalogowanych: 0, gości: 1