Czy to właściwe użycie #define, aby ułatwić pisanie powtarzanego kodu?

17

Czy istnieje pogląd na to, czy użycie #define do zdefiniowania pełnych wierszy kodu w celu uproszczenia kodowania jest dobrą czy złą praktyką programistyczną? Na przykład, gdybym musiał wydrukować kilka słów razem, zirytowałbym się na pisaniu

<< " " <<

Aby wstawić spację między słowami w instrukcji cout. Mógłbym po prostu zrobić

#define pSpace << " " <<

i wpisz

cout << word1 pSpace word2 << endl;

Dla mnie to nie dodaje ani nie odejmuje jasności kodu i sprawia, że ​​pisanie jest nieco łatwiejsze. Są inne przypadki, w których mogę wymyślić, gdzie pisanie będzie znacznie łatwiejsze, zwykle w celu debugowania.

Masz jakieś przemyślenia na ten temat?

EDYCJA: Dzięki za wszystkie świetne odpowiedzi! To pytanie przyszło mi do głowy po wielokrotnym pisaniu, ale nigdy nie myślałem, że będą inne, mniej mylące makra. Dla tych, którzy nie chcą czytać wszystkich odpowiedzi, najlepszą alternatywą jest użycie makr twojego IDE w celu ograniczenia powtarzania pisania.

gsingh2011
źródło
74
To dla ciebie jasne, ponieważ to wymyśliłeś. Dla wszystkich innych to tylko zaciemniło się. Na początku wygląda na błąd składniowy. Kiedy się skompiluje, pomyślałem, do diabła, a potem stwierdzę, że masz makro, którego nie ma we wszystkich wielkich literach. Moim zdaniem powoduje to, że utrzymanie kodu jest okropne. Zdecydowanie odrzuciłbym to, gdyby przyszedł do przeglądu kodu i nie oczekuję, że znajdziesz wielu, którzy by go zaakceptowali. I oszczędzasz 3 znaki !!!!!!!!!
Martin York,
11
Tam, gdzie nie można rozsądnie ocenić powtarzalności za pomocą funkcji lub czegokolwiek, lepszym podejściem jest nauczenie się, co może zrobić twój edytor lub IDE, aby ci pomóc. Makra edytora tekstu lub skróty „fragmentu” mogą zaoszczędzić Ci tyle pisania bez szkody dla czytelności.
Steve314,
2
Zrobiłem to już wcześniej (z większymi fragmentami płyty kotłowej), ale moją praktyką było napisanie kodu, a następnie uruchomienie preprocesora i zastąpienie oryginalnego pliku wyjściem preprocesora. Uratował mi pisanie na maszynie, oszczędził mi (i innym) kłopotów konserwacyjnych.
TMN
9
Uratowałeś 3 postacie i wymieniłeś je za mylące wypowiedzi. Bardzo dobrym przykładem złej makro, imho: o)
MaR
7
Wielu redaktorów ma zaawansowaną funkcję dokładnie dla tego scenariusza, nazywa się to „Kopiuj i wklej”
Chris Burt-Brown,

Odpowiedzi:

111

Pisanie kodu jest łatwe. Czytanie kodu jest trudne.

Piszesz kod raz. Żyje latami, ludzie czytają to sto razy.

Zoptymalizuj kod do odczytu, a nie do pisania.

tdammers
źródło
11
Zgadzam się w 100%. (W rzeczywistości zamierzałem napisać tę odpowiedź samodzielnie). Kod jest pisany raz, ale może być czytany dziesiątki, setki, a nawet tysiące razy, może dziesiątki, setki, a nawet tysiące programistów. Czas potrzebny na napisanie kodu jest całkowicie nieistotny, liczy się tylko czas na jego przeczytanie i zrozumienie.
sbi
2
Preprocesor może i powinien być wykorzystywany do optymalizacji kodu do odczytu i konserwacji.
SK-logic
2
I nawet jeśli to tylko ty czytasz kod za rok lub dwa: sam zapomnisz o tych rzeczach, robiąc inne rzeczy pomiędzy nimi.
johannes
2
„Zawsze koduj tak, jakby facet, który ostatecznie utrzyma twój kod, będzie brutalnym psychopatą, który wie, gdzie mieszkasz”. - (Martin Golding)
Dylan Yaga
@Dylan - w przeciwnym razie, po kilku miesiącach utrzymywania tego kodu, on będzie cię znaleźć - (Ja)
Steve314
28

Osobiście nie cierpię tego. Istnieje wiele powodów, dla których zniechęcam ludzi do tej techniki:

  1. W czasie kompilacji rzeczywiste zmiany kodu mogą być znaczące. Pojawia się następny facet, a nawet zawiera nawias zamykający w swoim #define lub wywołaniu funkcji. To, co jest napisane w pewnym punkcie kodu, jest dalekie od tego, co będzie tam po wstępnym przetwarzaniu.

  2. To jest nieczytelne. Może być dla ciebie jasne ... na razie ... jeśli to tylko ta jedna definicja. Jeśli stanie się to nawykiem, wkrótce skończysz z dziesiątkami #define i sam zaczniesz tracić orientację. Ale co najgorsze, nikt inny nie będzie w stanie zrozumieć, co word1 pSpace word2dokładnie oznacza (bez szukania #define).

  3. Może to stanowić problem dla narzędzi zewnętrznych. Powiedzmy, że w jakiś sposób otrzymałeś #define, która zawiera nawias zamykający, ale brak nawiasu otwierającego. Wszystko może działać dobrze, ale redaktorzy i inne narzędzia mogą postrzegać coś function(withSomeCoolDefine;raczej jako osobliwego (tj. Będą zgłaszać błędy i tak dalej). (Podobny przykład: wywołanie funkcji w definicji - czy twoje narzędzia analityczne będą w stanie znaleźć to wywołanie?)

  4. Konserwacja staje się znacznie trudniejsza. Wszystkie te definicje oprócz zwykłych problemów, jakie niesie ze sobą konserwacja. Oprócz powyższego punktu, negatywny wpływ może mieć również wsparcie narzędzi dla refaktoryzacji.

Szczery
źródło
4
W końcu zabroniłem sobie dostępu do prawie wszystkich makr z powodu kłopotów z obsługą ich bezpośrednio w Doxygen. Minęło już kilka lat i ogólnie myślę, że czytelność uległa znacznej poprawie - niezależnie od tego, czy używam Doxygen, czy nie.
Steve314,
16

Moją główną myślą na ten temat jest to, że podczas pisania kodu nigdy nie używam „ułatwiania pisania”.

Moją główną zasadą przy pisaniu kodu jest ułatwienie jego odczytu. Uzasadnieniem tego jest po prostu to, że kod jest odczytywany o rząd wielkości więcej razy, niż jest napisany. W związku z tym czas, który tracisz na pisanie go ostrożnie, uporządkowany, właściwie ułożony, jest w rzeczywistości inwestowany w dalsze czytanie i rozumienie o wiele szybciej.

W związku z tym #define, którego używasz, po prostu łamie zwykły sposób na przemian <<i inne rzeczy . Łamie zasadę najmniejszego zaskoczenia i nie jest dobrą rzeczą IMHO.

Didier Trosset
źródło
1
+1: „Kod jest odczytywany rząd wielkości więcej razy, niż jest zapisywany” !!!!
Giorgio
14

To pytanie daje jasny przykład tego, jak źle korzystać z makr. Aby zobaczyć inne przykłady (i się bawić), zobacz to pytanie .

Powiedziawszy to, podam przykłady z tego, co uważam za dobre włączenie makr.

Pierwszy przykład pojawia się w CppUnit , który jest strukturą testów jednostkowych. Jak każda inna standardowa platforma testowa, tworzysz klasę testową, a następnie musisz w jakiś sposób określić, które metody powinny być uruchamiane w ramach testu.

#include <cppunit/extensions/HelperMacros.h>

class ComplexNumberTest : public CppUnit::TestFixture  
{
    CPPUNIT_TEST_SUITE( ComplexNumberTest );
    CPPUNIT_TEST( testEquality );
    CPPUNIT_TEST( testAddition );
    CPPUNIT_TEST_SUITE_END();

 private:
     Complex *m_10_1, *m_1_1, *m_11_2;
 public:
     void setUp();
     void tearDown();
     void testEquality();
     void testAddition();
}

Jak widać, klasa ma blok makr jako pierwszy element. Jeśli dodam nową metodętestSubtraction , oczywiste jest, co musisz zrobić, aby włączyć ją do uruchomienia testowego.

Te bloki makr rozwijają się do czegoś takiego:

public: 
  static CppUnit::Test *suite()
  {
    CppUnit::TestSuite *suiteOfTests = new CppUnit::TestSuite( "ComplexNumberTest" );
    suiteOfTests->addTest( new CppUnit::TestCaller<ComplexNumberTest>( 
                                   "testEquality", 
                                   &ComplexNumberTest::testEquality ) );
    suiteOfTests->addTest( new CppUnit::TestCaller<ComplexNumberTest>(
                                   "testAddition",
                                   &ComplexNumberTest::testAddition ) );
    return suiteOfTests;
  }

Które wolisz czytać i utrzymywać?

Innym przykładem jest platforma Microsoft MFC, w której mapujesz funkcje na wiadomości:

BEGIN_MESSAGE_MAP( CMyWnd, CMyParentWndClass )
    ON_MESSAGE( WM_MYMESSAGE, OnMyMessage )
    ON_COMMAND_RANGE(ID_FILE_MENUITEM1, ID_FILE_MENUITEM3, OnFileMenuItems)
    // ... Possibly more entries to handle additional messages
END_MESSAGE_MAP( )

Jakie są zatem rzeczy, które odróżniają „Dobre makra” od okropnego zła?

  • Wykonują zadanie, którego nie można uprościć w żaden inny sposób. Pisanie makra w celu ustalenia maksimum między dwoma elementami jest niepoprawne, ponieważ można to zrobić przy użyciu metody szablonu. Ale są pewne złożone zadania (na przykład mapowanie kodów komunikatów na funkcje składowe), których język C ++ po prostu nie obsługuje elegancko.

  • Mają wyjątkowo surowe, formalne zastosowanie. W obu tych przykładach makra bloki są ogłaszane przez makra początkowe i końcowe, a makra między nimi będą pojawiać się tylko wewnątrz tych bloków. Masz normalny C ++, krótko usprawiedliwiasz się blokiem makr, a następnie wracasz do normy. W przykładach „złych makr” makra są rozproszone po całym kodzie, a nieszczęsny czytelnik nie ma możliwości dowiedzenia się, kiedy obowiązują reguły C ++, a kiedy nie.

Andrew Shepherd
źródło
5

Z pewnością będzie lepiej, jeśli dostroisz swój ulubiony edytor IDE / tekstu do wstawiania fragmentów kodu, które często męczą Cię przy ponownym wpisywaniu. I lepszy jest termin „uprzejmy” do porównania. Właściwie nie mogę myśleć o żadnym podobnym przypadku, gdy wstępne przetwarzanie bije makra edytora. Cóż, może być jeden - kiedy z jakichś tajemniczych i nieszczęśliwych powodów ciągle używasz innego zestawu narzędzi do kodowania. Ale to nie jest uzasadnienie :)

Może to być także lepsze rozwiązanie dla bardziej złożonych scenariuszy, gdy przetwarzanie wstępne tekstu może uczynić coś znacznie bardziej nieczytelnego i skomplikowanego (pomyśl o parametryzowanym wprowadzaniu).

shabunc
źródło
2
+1. Rzeczywiście: pozwól, aby redaktor wykonał pracę za Ciebie. Otrzymasz to, co najlepsze z obu światów, jeśli na przykład zrobisz skrót << " " <<.
unperson325680,
-1 dla „Może być także lepszym rozwiązaniem dla bardziej złożonych scenariuszy, gdy wstępne przetwarzanie tekstu może zrobić coś znacznie bardziej nieczytelnego i skomplikowanego (pomyśl o parametryzowanym wprowadzaniu)” - Jeśli jest to skomplikowane, stwórz dla niego metodę, nawet wtedy, metoda na to. np. to zło, które niedawno znalazłem w kodzie ..... # zdefiniować printError (x) {puts (x); return x}
mattnz
@mattnz, miałem na myśli konstrukcje pętlowe, konstrukcje if / else, szablony do tworzenia komparatorów i tak dalej - tego rodzaju rzeczy. W IDE takie sparametryzowane dane wejściowe pomagają nie tylko szybko wpisać kilka wierszy kodu, ale także szybko iterować parametry. Nikt nie próbuje konkurować metodami. metoda to metoda)))
shabunc
4

Inni wyjaśnili już, dlaczego nie należy tego robić. Twój przykład oczywiście nie zasługuje na wdrożenie za pomocą makra. Ale istnieje szeroki zakres przypadków, gdzie mają korzystać z makr dla zachowania czytelności.

Znanym przykładem mądrego zastosowania takiej techniki jest projekt Clanga : zobacz, jak .deftam wykorzystywane są pliki. Za pomocą makr #includemożesz podać jedną, czasem całkowicie deklaratywną definicję zbioru podobnych rzeczy, które zostaną rozwinięte w deklaracje typów,case instrukcje tam, gdzie to stosowne, domyślne inicjalizatory itp. Znacznie zwiększa to łatwość konserwacji: nigdy nie zapomnisz dodać nowego casewyciągi wszędzie po dodaniu nowegoenum , na przykład.

Tak jak w przypadku każdego innego potężnego narzędzia, musisz ostrożnie używać preprocesora C. W sztuce programowania nie ma ogólnych zasad, takich jak „nigdy nie powinieneś tego używać” lub „zawsze musisz tego używać”. Wszystkie zasady są jedynie wytycznymi.

Logika SK
źródło
3

Nigdy nie należy używać #definii w ten sposób. W twoim przypadku możesz to zrobić:

class MyCout 
{
public:
  MyCout (ostream &out) : m_out (out), m_space_pending (false)
  {
  }

  template <class T>
  MyCout &operator << (T &value)
  { 
    if (m_space_pending)
      m_out << " ";

    m_out << value;
    m_space_pending = false;
    return *this;
  }

  MyCout &operator << (const char *value)
  {
    if (m_space_pending)
      m_out << " ";

    m_out << value;
    m_space_pending = true;
    return *this;
  }

  MyCout &operator << (char *value) { return operator << (static_cast <const char *> (value)); }
  MyCout &operator << (ostream& (*fn)(ostream&)) { m_out << fn; return *this; }

private:
  ostream
    &m_out;

  bool
    m_space_pending;
};

int main (int argc, char *argv [])
{
  MyCout
    space_separated (cout);

  space_separated << "Hello" << "World" << endl;
}
Skizz
źródło
2

Nie.

W przypadku makr przeznaczonych do użycia w kodzie dobrą wskazówką do testowania stosowności jest otoczenie jej rozszerzenia nawiasami (dla wyrażeń) lub nawiasów klamrowych (dla kodu) i sprawdzenie, czy nadal będzie się kompilować:

// These don't compile:

#define pSpace (<< " " <<)
cout << word1 pSpace word2 << endl;

#define space(x) (" " << (x))
cout << word1 << space(word2) << endl;

// These do:

#define FOO_FACTOR (38)
x = y * FOO_FACTOR;

#define foo() (cout << "Foo" << endl)
foo();

#define die(c) { if ((c)) { exit(1); } }
die(foo > 8);

#define space(x) (" " + string((x)))
cout << "foo" << space("bar") << endl;

Makra używane w deklaracjach (jak przykład w odpowiedzi Andrew Shepherda) mogą uciec z luźniejszym zestawem reguł, o ile nie zakłócają otaczającego kontekstu (np. Przełączanie między publici private).

Blrfl
źródło
1

Jest to dość uzasadniona rzecz do zrobienia w czystym programie „C”.

Jest to niepotrzebne i mylące w programie C ++.

Istnieje wiele sposobów na uniknięcie powtarzającego się pisania kodu w C ++. Dzięki korzystaniu z udogodnień dostarczonych przez twoje IDE (nawet z vi prosty „ %s/ pspace /<< " " <</g” zaoszczędziłby tyle pisania i nadal generowałby standardowy czytelny kod). Możesz zdefiniować prywatne metody implementacji tego lub w bardziej skomplikowanych przypadkach szablon C ++ byłby czystszy i prostszy.

James Anderson
źródło
2
Nie, nie jest to racjonalnie słuszne w czystej C. Pojedyncze wartości lub pełne niezależne wyrażenia, które opierają się tylko na makroparametrach, tak, a dla tych ostatnich funkcja może być nawet lepszym wyborem. Taki na wpół upieczony konstrukt jak w przykładzie, nie ma mowy.
Zabezpiecz
@secure - Zgadzam się, że w przypadku podanego przykładu nie jest to dobry pomysł. Ale biorąc pod uwagę brak szablonów itp., Makra „#DEFINE” w C. są prawidłowe.
James Anderson
1

W C ++ można to rozwiązać poprzez przeciążenie operatora. Lub nawet coś tak prostego jak funkcja variadic:

lineWithSpaces(word1, word2, word3, ..., wordn)jest zarówno prosty, jak i oszczędza ciągłego pisania pSpaces.

Chociaż w twoim przypadku może to nie wydawać się wielkim problemem, istnieje rozwiązanie, które jest prostsze i bardziej niezawodne.

Ogólnie rzecz biorąc, jest kilka przypadków, w których użycie makra jest znacznie krótsze bez wprowadzania zaciemnienia, a przeważnie istnieje wystarczająco krótkie rozwiązanie wykorzystujące rzeczywiste funkcje językowe (makra są raczej zwykłym zastąpieniem łańcucha).

back2dos
źródło
0

Czy istnieje pogląd na to, czy użycie #define do zdefiniowania pełnych wierszy kodu w celu uproszczenia kodowania jest dobrą czy złą praktyką programistyczną?

Tak, jest bardzo źle. Widziałem nawet ludzi, którzy to robią:

#define R return

aby zapisać pisanie (to, co próbujesz osiągnąć).

Taki kod należy tylko w takich miejscach jak to .

BЈовић
źródło
-1

Makra są złe i powinny być używane tylko wtedy, gdy naprawdę musisz. Istnieje kilka przypadków, w których obowiązują makra (głównie debugowanie). Ale w C ++ w większości przypadków można zamiast tego użyć funkcji wbudowanych .

sakisk
źródło
2
W żadnej technice programowania nie ma nic zła. Można używać wszystkich możliwych narzędzi i metod, o ile wiesz, co robisz. Dotyczy to niesławnego goto, wszystkich możliwych systemów makro itp.
SK-logic
1
To jest dokładnie definicja zła w tym przypadku: „coś, czego powinieneś unikać przez większość czasu, ale nie coś, czego powinieneś unikać przez cały czas”. Wyjaśnia to link, który wskazuje zło .
sakisk
2
Uważam, że oznaczanie czegokolwiek jako „złego”, „potencjalnie szkodliwego” lub nawet „podejrzanego” przynosi efekt przeciwny do zamierzonego. Nie podoba mi się pojęcie „złej praktyki” i „zapachu kodu”. Każdy programista musi w każdym konkretnym przypadku zdecydować, która praktyka jest szkodliwa. Etykietowanie jest szkodliwą praktyką - ludzie nie myślą dalej, jeśli coś zostało już oznakowane przez innych.
SK-logic
-2

Nie, nie możesz używać makr do zapisywania tekstu .

Jednak jesteś dozwolony, nawet wymagane jest użycie ich do oddzielenia niezmiennej części kodu od zmiany i zmniejszenia nadmiarowości. W tym drugim przypadku musisz pomyśleć o alternatywach i wybrać makro tylko wtedy, gdy lepsze narzędzia nie działają. (W przypadku makra ćwiczeniowego jest dość na końcu linii, więc posiadanie go oznacza ostateczność ...)

Aby ograniczyć pisanie, większość edytorów ma makra, nawet inteligentne fragmenty kodu.

Balog Pal
źródło
„Nie, nie możesz używać makr do zapisywania tekstu ”. - Dobra robota, nie będę tutaj słuchać twojego zamówienia! Jeśli mam stos obiektów, które muszę zadeklarować / zdefiniować / mapę / switch/ itd., Możesz się założyć, że użyję makr, aby uniknąć szaleństwa - i zwiększyć czytelność. Używanie makr do zapisywania słów kluczowych, kontroli przepływu i tym podobnych jest głupie - ale powiedzenie, że nikt nie może ich używać do zapisywania naciśnięć klawiszy w prawidłowych kontekstach, jest równie oczywiste.
underscore_d