Wydaje mi się, że mam mieszany kod C i C ++, kiedy nie powinienem; Czy to problem i jak go naprawić?

10

Tło / scenariusz

Zacząłem pisać aplikację CLI wyłącznie w C (mój pierwszy właściwy program w C lub C ++, który nie był „Hello World” ani jego odmianą). Mniej więcej w połowie pracowałem z „ciągami znaków” wprowadzanymi przez użytkownika (tablice znaków) i odkryłem obiekt streamera ciągów znaków C ++. Widziałem, że mogę zapisać kod, używając ich, więc użyłem ich za pośrednictwem aplikacji. Oznacza to, że zmieniłem rozszerzenie pliku na .cpp i teraz kompiluję aplikację g++zamiast gcc. Opierając się na tym, powiedziałbym, że aplikacja jest teraz technicznie aplikacją C ++ (chociaż 90% + kodu jest napisane w tym, co nazwałbym C, ponieważ istnieje wiele skrzyżowań między tymi dwoma językami, biorąc pod uwagę moje ograniczone doświadczenie w dwójka). Jest to pojedynczy plik .cpp o długości około 900 linii.

Ważne czynniki

Chcę, aby program był darmowy (jak w pieniądzu), mógł być swobodnie dystrybuowany i użyteczny dla wszystkich. Obawiam się, że ktoś spojrzy na kod i pomyśli coś z efektem:

Och, spójrz na kodowanie, to okropne, ten program nie może mi pomóc

Gdy potencjalnie może! Inną kwestią jest efektywność kodu (jest to program do testowania łączności Ethernet). Nie powinno być żadnych części kodu, które byłyby tak nieefektywne, że mogłyby poważnie utrudnić działanie aplikacji lub jej wyników. Myślę jednak, że to pytanie dotyczy przepełnienia stosu, gdy prosi o pomoc w zakresie określonych funkcji, metod, wywołań obiektów itp.

Moje pytanie

Posiadanie (moim zdaniem) mieszania C i C ++ tam, gdzie być może nie powinienem. Powinienem spróbować przepisać to wszystko w C ++ (mam na myśli implementację większej liczby obiektów i metod C ++, w których być może kodowałem coś w stylu C, które można skondensować przy użyciu nowszych technik C ++), lub usunąć użycie obiektów do streamingu łańcuchów i przywrócić to wszystko z powrotem do kodu C? Czy jest tu właściwe podejście? Jestem zagubiony i potrzebuję wskazówek, jak zachować tę aplikację „dobrą” w oczach mas, aby mogli z niej korzystać i czerpać z niej korzyści.

Kod - aktualizacja

Oto link do kodu. To około 40% komentarzy, komentuję prawie każdą linię, dopóki nie poczuję się bardziej płynnie. W kopii, do której odsyłam, usunąłem prawie wszystkie komentarze. Mam nadzieję, że nie utrudni to czytania. Mam jednak nadzieję, że nikt nie powinien tego w pełni rozumieć. Jeśli jednak popełniłem fatalne wady projektowe, mam nadzieję, że powinny być łatwe do zidentyfikowania. Powinienem także wspomnieć, że piszę kilka komputerów stacjonarnych i laptopów Ubuntu. Nie zamierzam przenosić kodu do innych systemów operacyjnych.

jwbensley
źródło
3
Ujednoznaczniłem CLI dla ciebie. Interfejs CLI może również odnosić się do infrastruktury Common Language Infrastructure, co nie ma większego sensu z perspektywy C.
Robert Harvey
Możesz przepisać go w FORTRAN. Nigdy nie słyszałem o OOF.
ott--
2
Nie martwiłbym się zbytnio ludźmi, którzy nie używają twojego oprogramowania, jeśli nie jest ono „ładne”. 99% użytkowników nie będzie nawet patrzeć na kod ani przejmować się jego pisaniem, dopóki osiągnie to, czego potrzebują. Ważne jest jednak, aby kod był spójny itp., Aby pomóc Ci w utrzymaniu go w dłuższej perspektywie.
Evicatos,
1
Dlaczego nie uczynisz swojej rzeczy wolnym oprogramowaniem (np. Na licencji GPLv3 ), z kodem na np . Github . W kodzie brakuje LICENSEpliku. Możesz uzyskać ciekawe informacje zwrotne.
Basile Starynkevitch

Odpowiedzi:

13

Zacznijmy od początku: mieszany kod C i C ++ jest dość powszechny. Więc na początek jesteś w dużym klubie. Na wolności mamy ogromne bazy kodów C. Ale z oczywistych powodów wielu programistów odmawia pisania co najmniej nowych rzeczy w C, mając dostęp do C ++ w tym samym kompilatorze, nowe moduły zaczynają być pisane w ten sposób - na początku po prostu pozostawiając istniejące części w spokoju.

Następnie niektóre istniejące pliki zostaną ponownie skompilowane jako C ++, a niektóre mosty można usunąć ... Ale może to potrwać naprawdę długo.

Trochę wyprzedzasz, twój pełny system to teraz C ++, tylko większość z nich jest napisana w stylu „C”. I widzisz, że mieszanie stylów stanowi problem, czego nie powinieneś: C ++ jest językiem o wielu paradygmatach, obsługującym wiele stylów i pozwalającym im na dobre współistnienie. W rzeczywistości jest to główna siła, że ​​nie jesteś zmuszony do jednego stylu. Taki, który byłby nieoptymalny tu i tam, przy odrobinie szczęścia nie wszędzie.

Ponowna praca bazy kodu jest dobrym pomysłem, JEŚLI jest zepsuta. Lub jeśli jest to na drodze rozwoju. Ale jeśli to działa (w oryginalnym znaczeniu tego słowa), należy postępować zgodnie z najbardziej podstawową zasadą inżynierii: jeśli się nie zepsuło, nie naprawiaj go. Pozostaw zimne części w spokoju, włóż swój wysiłek tam, gdzie się liczy. Na częściach, które są złe, niebezpieczne - lub w nowych funkcjach, po prostu przerób części, aby stały się łóżkiem.

Jeśli szukasz ogólnych rzeczy do rozwiązania, oto, co warto eksmitować z bazy kodu C:

  • wszystkie funkcje str * i char [] - zamień je na klasę łańcuchową
  • jeśli używasz sprintf, utwórz wersję, która zwraca ciąg z wynikiem lub umieści go w ciągu, i zastąp użycie. (Jeśli nigdy nie przejmowałeś się strumieniami, wyświadcz sobie przysługę i po prostu pomiń je, chyba że ci się podobają; gcc zapewnia doskonałe bezpieczeństwo typu po wyjęciu z pudełka podczas sprawdzania formatów, po prostu dodaj odpowiedni atrybut.
  • najbardziej malloc i za darmo - NIE z nowymi i usuwanymi, ale wektorami, listami, mapami i innymi kolekcjonerami.
  • reszta zarządzania pamięcią (po dwóch poprzednich punktach musi to być dość rzadkie, przykryć inteligentnymi wskaźnikami lub zaimplementować swoje specjalne kolekcje
  • zastąp wszystkie inne użycie zasobów (PLIK *, muteks, blokada itp.), aby użyć opakowań lub klas RAII

Kiedy skończysz z tym, zbliżasz się do punktu, w którym podstawa kodu może być w zasadzie bezpieczna dla wyjątków, więc możesz upuścić futbolowy kod powrotu, używając wyjątków i rzadkiego try / catch tylko w funkcjach wysokiego poziomu.

Poza tym po prostu napisz nowy kod w jakimś zdrowym C ++, a jeśli rodzą się pewne klasy, które są dobrym zamiennikiem w istniejącym kodzie, podnieś je.

Nie wspominałem o rzeczach związanych ze składnią, oczywiście używam referencji zamiast wskaźników w nowym kodzie, ale zastępowanie starych części C tylko dla tej zmiany nie jest dobrą wartością. Odlewy, które musisz rozwiązać, wyeliminować wszystko, co możesz, i użyć wariantów C ++ w funkcjach otoki dla pozostałej części. I bardzo ważne, dodaj const wszędzie tam, gdzie ma to zastosowanie. Przeplatają się one z wcześniejszymi pociskami. I konsoliduj swoje makra i zamień to, co możesz zrobić na wyliczanie, funkcję wbudowaną lub szablon.

Sugeruję przeczytanie standardów kodowania C ++ Sutter / Alexandrescu, jeśli jeszcze tego nie zrobiono, i ściśle ich przestrzegać.

Balog Pal
źródło
Wielkie dzięki za wejście Balog, wszystkie rozsądne porady w moich oczach i zgadzam się z tym wszystkim. Myślę, że będę powoli zmieniać sekcję kodu według sekcji, nadając priorytet działającemu kodowi.
jwbensley
7

Krótko mówiąc: nie pójdziesz do piekła, nic złego się nie stanie, ale nie wygrasz też żadnych konkursów piękności.

Chociaż użycie C ++ jako „lepszego C” jest całkowicie możliwe, odrzucasz wiele zalet C ++, ale ponieważ nie ograniczasz się do wanilii C, nie zyskujesz żadnych korzyści z C (prostota, przenośność , przejrzystość, interoperacyjność). Innymi słowy: C ++ poświęca niektóre cechy C, aby zyskać inne - na przykład przezroczystość C, w której zawsze można wyraźnie zobaczyć, gdzie i kiedy następuje alokacja pamięci, jest wymieniana na silniejsze abstrakcje C ++.

Ponieważ wydaje się, że Twój kod działa teraz poprawnie, prawdopodobnie nie jest dobrym pomysłem przepisanie go tylko ze względu na to: zachowaj go takim, jakim jest na teraz, i zmień go na bardziej idiomatyczny C ++ w miarę upływu czasu, po jednym kawałku, za każdym razem, gdy pracujesz nad jakąkolwiek częścią. I pamiętaj o wyciągniętych wnioskach o następnym projekcie, przede wszystkim o tym, że C i C ++ nie są tym samym językiem, i lepiej jest dokonać wyboru z góry, niż decydować się na przejście do C ++ w połowie projektu .

tdammers
źródło
Dzięki tdammers za radę. Zgadzam się z tym, co mówisz i biorę to na pokład, dzięki!
jwbensley
4

C ++ jest bardzo złożonym językiem, w tym sensie, że ma wiele funkcji. Jest to również język, który obsługuje wiele paradygmatów, co oznacza, że ​​pozwala pisać całkowicie kod proceduralny bez użycia żadnych obiektów.

Ponieważ C ++ jest tak złożony, często widzisz, że ludzie używają ograniczonego podzbioru jego funkcji w swoim kodzie. Początkujący często używają po prostu strumieniowego wejścia / wyjścia, obiektów łańcuchowych i nowego / delete zamiast malloc / free, a może referencji zamiast wskaźników. Gdy poznasz funkcje obiektowe, możesz zacząć pisać w stylu „C z klasami”. W końcu, gdy dowiesz się więcej o C ++, zaczniesz używać szablonów, RAII , STL , inteligentnych wskaźników itp.

Chodzi mi o to, że nauka C ++ jest procesem wymagającym czasu. Tak, teraz twój kod prawdopodobnie wygląda tak, jakby został napisany przez programistę C próbującego napisać C ++. A ponieważ dopiero się uczysz, to jest całkowicie w porządku. Czuję jednak dreszcze, gdy widzę coś takiego w kodzie produkcyjnym napisanym przez doświadczonych programistów, którzy powinni wiedzieć lepiej.

Pamiętaj tylko, że dobry programista Fortran może pisać dobry kod Fortran w dowolnym języku. :)

Dima
źródło
2

Wygląda na to, że podsumowujesz dokładnie ścieżkę wielu starych programistów C, przechodząc do C ++. Używasz go jako „lepszego C”. Dwadzieścia lat temu miało to jakiś sens, ale w tym momencie w C ++ jest tak wiele potężniejszych konstrukcji (jak Standardowa Biblioteka Szablonów), że obecnie rzadko ma to sens. Przynajmniej powinieneś wykonać następujące czynności, aby uniknąć dawania tętniaków programistom C ++, gdy patrzysz na swój kod:

  • Używać strumieni zamiast? printfrodzina.
  • Użyj newzamiast malloc.
  • Użyj klas kontenerów STL dla wszystkich struktur danych.
  • Użyj std::stringzamiast char*tam, gdzie to możliwe
  • Zrozum i używaj RAII

Jeśli twój program jest krótki (a ~ 900 wierszy wydaje się krótki) osobiście nie sądzę, aby próba stworzenia solidnego zestawu klas była wymagana, a nawet przydatna.

Gort the Robot
źródło
Dzięki za radę Steven, tak, miałem taki sam pomysł na temat zajęć i myślę, że mogę uporządkować kod w jeden czysty plik. Dzięki!
jwbensley
2

Przyjęta odpowiedź wymienia tylko zalety konwersji C na idiomatyczny C ++, tak jakby kod C ++ był w pewnym sensie absolutnie lepszy niż kod C. Zgadzam się z innymi odpowiedziami, że prawdopodobnie nie jest konieczne wprowadzanie drastycznych zmian w mieszanym kodzie, jeśli nie jest uszkodzony.

To, czy mieszanie C i C ++ jest zrównoważone, zależy od sposobu mieszania. Subtelne błędy można wprowadzić na przykład, gdy w kodzie C zgłaszane są wyjątki (które nie są bezpieczne), powodując wycieki pamięci lub uszkodzenia danych. Bezpieczniejszym i dość powszechnym sposobem jest owijanie bibliotek C lub interfejsów w klasy lub używanie ich do innego rodzaju izolacji w projekcie C ++.

Zanim zdecydujesz się przepisać cały projekt na idiomatic C ++ (lub C), powinieneś mieć świadomość, że wiele zmian przedstawionych w innych odpowiedziach może spowolnić program lub wywołać inne niepożądane efekty. Na przykład:

  • zmiana alokowanych przez stos ciągów C na std :: strings może powodować niepotrzebne przydzielanie sterty
  • zmiana surowych wskaźników na niektóre wspólne typy wskaźników (takie jak std :: shared_ptr) powoduje narzut dostępu z powodu liczenia referencji, wewnętrznych funkcji wirtualnych elementów i bezpieczeństwa wątków
  • standardowe strumienie biblioteczne są wolniejsze niż odpowiedniki C.
  • nieostrożne korzystanie z klas RAII, takich jak kontenery, może powodować niepotrzebne operacje, szczególnie gdy nie można użyć semantyki przenoszenia C ++ 11
  • szablony mogą powodować dłuższe czasy kompilacji, niejasne błędy kompilacji, rozdęcie kodu i problemy z przenośnością między kompilatorami
  • pisanie kodu bezpiecznego dla wyjątków jest trudne, co ułatwia wprowadzanie subtelnych błędów podczas konwersji
  • trudniej jest użyć kodu C ++ z biblioteki współużytkowanej niż zwykłego C.
  • C ++ nie jest tak szeroko wspierany jak np. C89

Podsumowując, konwersję mieszanego kodu C i C ++ do idiomatycznego C ++ należy postrzegać jako kompromis, który ma o wiele więcej niż tylko czas implementacji i powiększenie zestawu narzędzi o pozornie wygodne funkcje. Dlatego bardzo trudno jest udzielić odpowiedzi w ogólnym przypadku innym niż „to zależy”.

crafn
źródło
„standardowe strumienie biblioteczne są wolniejsze niż odpowiedniki w języku C”: Prawdopodobnie, z pewnością trudniejsze do internacjonalizacji niż printf.
Deduplicator
1

Mieszanie C i C ++ to zła forma. Są to odrębne języki i naprawdę powinny być traktowane jako takie. Wybierz ten, który najbardziej ci odpowiada i spróbuj napisać kod idiomatyczny w tym języku.

Jeśli C ++ oszczędza ci dużo kodu, trzymaj się C ++ i ponownie napisz części C. Wątpię, czy różnica w wydajności będzie zauważalna, jeśli o to się martwisz.

Oleksi
źródło
Mam więc jedną bibliotekę, z której chcę korzystać, która jest napisana w C, i mam inną bibliotekę, z której chcę korzystać, która jest napisana w C ++ ... Przepisywanie kodu, który działa dobrze, to po prostu tworzenie niepotrzebnej pracy.
gnasher729,
1

Cały czas poruszam się między C i C ++ i jestem dziwakiem, który woli pracować w ten sposób. Wolę C ++ do rzeczy na wyższym poziomie, podczas gdy używam C jako tępego narzędzia, które pozwala mi prześwietlać typy danych i traktować je jak bity i bajty za pomocą, powiedzmy, memcpyprzydatnej do implementacji struktur danych niskiego poziomu i alokatorów pamięci . Jeśli czujesz, że pracujesz na poziomie surowych bitów i bajtów, to naprawdę bogaty system typów C ++ w ogóle nic nie pomaga i często łatwiej jest mi napisać taki kod w C, gdzie mogę bezpiecznie założyć, że mogę traktuj dowolny typ danych C jak tylko bity i bajty.

Najważniejszą rzeczą do zapamiętania jest to, że te dwa języki nie łączą się dobrze.

1. Funkcja AC nigdy nie powinna wywoływać funkcji C ++, która może throw. Biorąc pod uwagę liczbę miejsc, w których twój codzienny kod C ++ może pośrednio wystąpić jako wyjątek, oznacza to ogólnie, że twoje funkcje C nie powinny ogólnie wywoływać funkcji C ++. W przeciwnym razie kod C nie będzie w stanie zwolnić zasobów, które alokuje podczas rozwijania stosu, ponieważ nie ma praktycznego sposobu na złapanie wyjątku C ++. Jeśli w końcu będziesz potrzebować kodu C do wywołania funkcji C ++ jako wywołania zwrotnego, np. Strona C ++ powinna upewnić się, że każdy napotkany wyjątek zostanie wychwycony przed powrotem do kodu C.

2. Jeśli napiszesz kod C, który traktuje typy danych jako zwykłe bity i bajty, spychając system typów (to właściwie mój główny powód używania C w pierwszej kolejności), to nigdy nie chcesz używać takiego kodu w stosunku do danych C ++ typy, które mogą mieć konstruktory kopiujące i destruktory oraz wirtualne wskaźniki i tego rodzaju rzeczy, których należy przestrzegać. Ogólnie więc powinien to być kod C używający tego typu kodu C, a nie kod C ++ używający tego typu kodu C.

Jeśli chcesz, aby Twój kod C ++ używał takiego kodu C, to zazwyczaj chcesz go użyć jako szczegółów implementacyjnych klasy, która upewnia się, że dane, które są przechowywane w ogólnej strukturze danych C, są typem danych, które są łatwe do zbudowania i zniszczyć. Na szczęście istnieją cechy typu jak ten , który można użyć do sprawdzenia, czy w assert statycznej, upewniając się, że typy są przechowywane w strukturze rodzajowej C dane są trywialne destruktorów i konstruktorów i pozostanie w ten sposób.

Powinienem spróbować przepisać to wszystko w C ++ (mam na myśli implementację większej liczby obiektów i metod C ++, w których być może kodowałem coś w stylu C, które można skondensować przy użyciu nowszych technik C ++), lub usunąć użycie obiektów do streamingu łańcuchów i przywrócić to wszystko z powrotem do kodu C?

Moim zdaniem nie musisz się martwić, jeśli będziesz przestrzegać powyższych zasad i upewnisz się, że kod jest dobrze przetestowany pod kątem testów, które napisałeś. Część, którą warto ulepszyć, to kod napisany do tej pory w C ++, ale to nie znaczy, że musisz przenieść kod ściśle oparty na C na C ++.

Z kodem, który napisałeś w C ++ i kompilujesz jako kod C ++, musisz być ostrożny. Używanie kodowania podobnego do C w C ++ wymaga problemów. Na przykład, jeśli przydzielasz i zwalniasz zasoby ręcznie, istnieje szansa, że ​​Twój kod nie jest bezpieczny pod kątem wyjątków, ponieważ kod może napotkać wyjątek, w którym niejawnie wyjdziesz z funkcji, zanim zdołasz kiedykolwiek freezasób. W C ++, RAII i rzeczy takie jak inteligentne wskaźniki nie są puszystą wygodą, ponieważ mogą się pojawić, jeśli spojrzysz tylko na normalne ścieżki wykonywania bez uwzględnienia wyjątkowych ścieżek. Często są podstawową koniecznością pisania poprawnego kodu bezpiecznego dla wyjątków w prosty i skuteczny sposób, który nie zacznie przeciekać wszędzie po napotkaniu wyjątku.


źródło