為什麼很多大牛在寫題的時候要加一堆宏?

看到過bc以及一些其他平台上的代碼,很多人都是寫了一堆宏。這樣的話不是除了自己,別人都很難理解嗎?求解釋為什麼。。。


給你說幾個 inline 無法代替宏的地方:

1. 循環展開:

// loop unroll double
#define LOOP_UNROLL_DOUBLE(action, actionx2, width) do {
unsigned long __width = (unsigned long)(width);
unsigned long __increment = __width &>&> 2;
for (; __increment &> 0; __increment--) {
actionx2;
actionx2;
}
switch (__width 3) {
case 2: actionx2; break;
case 3: actionx2;
case 1: action; break;
}
} while (0)

// loop unroll quatro
#define LOOP_UNROLL_QUATRO(action, actionx2, actionx4, width) do {
unsigned long __width = (unsigned long)(width);
unsigned long __increment = __width &>&> 2;
for (; __increment &> 0; __increment--) {
actionx4;
}
switch (__width 3) {
case 2: actionx2; break;
case 3: actionx2;
case 1: action; break;
}
} while (0)

假設你需要高速循環做一個事情,那麼展開循環可以極大的減少CPU分支,並且充分利用CPU流水線的並行效果,比如你開發一個 FIR濾波器來處理信號,那麼你的代碼如果從 for (...) { .... } 變成循環展開的話,可以這麼寫:

LOOP_UNROLL_DOUBLE(
{
x = *src++;
// do something with x and h and output to y
*dst++ = y;
},
{
x1 = *src++;
x2 = *src++;
// do something with x1 and h and output to y1
// do something with x2 and h and output to y2
*dst++ = y1;
*dst++ = y2;
},
nsamples,
);

如此寫法將每個循環只計算一個 sample,變為每個循環同時計算兩個sample,分開寫代碼,也能更好的利用 SIMD去加速同時多個 sample的計算過程,這就是利用循環展開來優化性能的用法,直接傳 "{...}" 裡面的運行代碼給宏,宏不變,但是每處使用LOOP_UNROLL的地方 "{.. } " 中的代碼都是不同的,inline是代替不了的,你總不至於傳個函數指針過去吧,這時性能優化方面情況。

2. 函數組裝:

想像一下,你寫圖形圖像的代碼,現在你需要給像素合成實現 SRC_ATOP, SRC_OVER, SRC_IN, SRC_OUT, DST_ATOP, DST_OVER, DST_IN, DST_OUT, XOR, PLUS, ALLANON, TINT, DIFF, DARKEN, LIGHTEN, SCREEN, OVERLAY 等等 二十種像素合成的方法,你如果不用宏,那麼你需要寫多少個函數?20多個看起來類似的函數,你不得寫瘋了么?此時用函數指針其實是很浪費性能的事情,那麼該如何寫呢?你可以規定一系列用來計算composite的方法,接受兩組 RGBA,生成新的,比如:

/* compositing */
#define IBLEND_COMPOSITE(sr, sg, sb, sa, dr, dg, db, da, FS, FD) do {
(dr) = _ipixel_mullut[(FS)][(sr)] + _ipixel_mullut[(FD)][(dr)];
(dg) = _ipixel_mullut[(FS)][(sg)] + _ipixel_mullut[(FD)][(dg)];
(db) = _ipixel_mullut[(FS)][(sb)] + _ipixel_mullut[(FD)][(db)];
(da) = _ipixel_mullut[(FS)][(sa)] + _ipixel_mullut[(FD)][(da)];
} while (0)

/* premultiply: src over */
#define IBLEND_OP_SRC_OVER(sr, sg, sb, sa, dr, dg, db, da) do {
IUINT32 FD = 255 - (sa);
IBLEND_COMPOSITE(sr, sg, sb, sa, dr, dg, db, da, 255, FD);
} while (0)

/* premultiply: dst atop */
#define IBLEND_OP_DST_ATOP(sr, sg, sb, sa, dr, dg, db, da) do {
IUINT32 FS = 255 - (da);
IUINT32 FD = (sa);
IBLEND_COMPOSITE(sr, sg, sb, sa, dr, dg, db, da, FS, FD);
} while (0)

/* premultiply: dst in */
#define IBLEND_OP_DST_IN(sr, sg, sb, sa, dr, dg, db, da) do {
IUINT32 FD = (sa);
IBLEND_COMPOSITE(sr, sg, sb, sa, dr, dg, db, da, 0, FD);
} while (0)

然後用 #連接各種方法和格式,生成不同的函數,比如:

#define IPIXEL_COMPOSITE_FN(name, opname)
static void ipixel_comp_##name(IUINT32 *dst, const IUINT32 *src, int w)
{
IUINT32 sr, sg, sb, sa, dr, dg, db, da;
for (; w &> 0; dst++, src++, w--) {
_ipixel_load_card(src, sr, sg, sb, sa);
_ipixel_load_card(dst, dr, dg, db, da);
IBLEND_OP_##opname(sr, sg, sb, sa, dr, dg, db, da);
dst[0] = IRGBA_TO_A8R8G8B8(dr, dg, db, da);
}
}

然後開始生成我們的各種合成函數:

IPIXEL_COMPOSITE_PREMUL(pre_xor, XOR);
IPIXEL_COMPOSITE_PREMUL(pre_plus, PLUS);
IPIXEL_COMPOSITE_PREMUL(pre_src_atop, SRC_ATOP);
IPIXEL_COMPOSITE_PREMUL(pre_src_in, SRC_IN);
IPIXEL_COMPOSITE_PREMUL(pre_src_out, SRC_OUT);
IPIXEL_COMPOSITE_PREMUL(pre_src_over, SRC_OVER);
IPIXEL_COMPOSITE_PREMUL(pre_dst_atop, DST_ATOP);
IPIXEL_COMPOSITE_PREMUL(pre_dst_in, DST_IN);
IPIXEL_COMPOSITE_PREMUL(pre_dst_out, DST_OUT);
IPIXEL_COMPOSITE_PREMUL(pre_dst_over, DST_OVER);

這樣你相當於定義了:

ipixel_comp_pre_xor (...)
ipixel_comp_pre_plus (...)
....
ipixel_comp_dst_over (...)

等好幾個函數了,並且這些函數都是被你 「組裝」 出來的,你並沒有使用函數指針,也沒有笨重的去寫20多個函數。進一步如果你寫圖形圖像你會發現你需要面對多種設備的像素格式,從 A8R8G8B8, A8B8G8R8 到 A1R5G5B5 , 主流需要處理的像素格式都有10多種。

那麼你可以把 「從不同格式讀取 r,g,b,a」, 以及 「將 r,g,b,a組裝成任意格式」,展開成很多個宏,然後不管你在這些像素格式裡面做轉換還是要做一些其他處理,你都可以用任意的 「像素讀寫」 宏 + 「像素計算」 宏 組裝成一個個具體需要的函數。

所以用宏來解決性能問題,並且簡化自己的程序設計往往能起到 inline不能起的作用,甚至能完成很多 template 所不能完成的任務。

3. 數據結構和演算法:

具體可以參考 Linux Kernel的 include/linux/list.h:

struct list_head {
struct list_head *next, *prev;
};

#define INIT_LIST_HEAD(ptr) do {
(ptr)-&>next = (ptr); (ptr)-&>prev = (ptr);
} while (0)

/*
* Insert a new entry between two known consecutive entries.
*
* This is only for internal list manipulation where we know
* the prev/next entries already!
*/
static __inline__ void __list_add(struct list_head * new,
struct list_head * prev,
struct list_head * next)
{
next-&>prev = new;
new-&>next = next;
new-&>prev = prev;
prev-&>next = new;
}

這裡定義了一個 LIST,kernel中,能用 inline的地方都用了,但是有些地方用不了,比如,你有一個結構體 (netfilter 部分):

struct nf_hook_ops
{
struct list_head list;

/* User fills in from here down. */
nf_hookfn *hook;
int pf;
int hooknum;
/* Hooks are ordered in ascending priority. */
int priority;
};

然後你有一個鏈表,記錄著很多 nf_hook_ops,你取到了其中一個節點的指針,其實是指向結構體的 list這個成員的,你需要得到對應結構體的指針,那麼你可以用下面的 list 的宏:

/**
* list_entry - get the struct for this entry
* @ptr: the struct list_head pointer.
* @type: the type of the struct this is embedded in.
* @member: the name of the list_struct within the struct.
*/
#define list_entry(ptr, type, member)
((type *)((char *)(ptr)-(unsigned long)(((type *)0)-&>member)))

比如,list_entry(ptr, struct nf_hook_ops, list) 就能根據節點指針,和在某個 struct裡面的位置,取到某個節點對應的 struct的指針了。這個做法,用 inline也是沒法做的。

同樣的應用,在 Kernel中,還有紅黑樹 rbtree.h,rbtree.c中的實現,和 list很類似,大量的宏應用。Linux 用基礎的宏實現的 list, rbtree等基礎數據結構,用起來是相當方便的,有些地方比 std::list, std::map 都方便多了,比 STL性能高的同時,避免象模版一樣為每種類型生成不同的代碼,讓你的二進位程序變得很臃腫。

比如你在做題的時候,用上了這樣的數據結構,你程序會比用 stl容器的代碼更高效和精簡,同時你不知道目標平台 STL是怎麼實現的,你無法控制,明明我在這個平台寫著很快的代碼,為何換個平台又慢了,為了追求究極性能,這樣重新定義數據結構,其實是可以理解的。

4. 其他 inline 無法代替宏的地方

  • 針對不同平台特性(比如整數是32還是64,lsb還是msb)寫出的優化代碼。

  • 泛型的模擬

  • 小型高頻重複的代碼片

  • 硬體操作的定義

等等,很多情況,inline或者 template還是無法把宏給代替了,所以很多開源項目的代碼裡面,大量的出現各種宏,主要是出於這些方面的考慮。

-----------


感覺很多回答並沒有答到題上,包括最高票的。。。根據問題標籤和題主提到的BC,題主問的應該是做ACM題時候為什麼要用那麼多的宏。。。

最直接的原因應該就是提高手速了,這是大多數宏定義在演算法競賽中起的作用,其中還有一大部分是單純的字元串替換,舉個栗子:

for(int i=0;i&

如果你有提前寫好的宏定義的模板的話。。。

rep(i,0,n)
vec.pb(mp(p[i].X,-p[i].Y));

。。。像我這種懶星人一般會覺得下面那個看起來又省事又好讀,上面那個是啥啊_(:3」∠)_

而且不只是宏定義啊,還有各種typedef啊:

typedef pair& pii;
typedef long long ll;//有人可能喜歡#define ll long long

還有各種模板里的常用小函數啊,gcd和pow之類的,提前寫好了基本都是為了後面比賽用著省事的。

當然這要看個人習慣了。。。不過,一般ACMer用宏定義的,基本上90%會把一些常用的給定義成這樣:push_back=pb/PB,make_pair=mp/MP,first=X/x/fst,second=Y/y/snd。。。以及各種for的寫法,rep,FOR,foreach,forIt。。。只要比賽打多了不用看他模板的define怎麼寫的都能直接認出來(for有時候還是要稍微注意下的,比如及其少數的某些幾百行的模板里寫了20多種for的宏定義的。。。)

以及,寫演算法題的代碼主要還是為了過題,又不是為了讓別人看懂的,所以別人看不看得懂並不關我什麼事。。。要寫題解貼上去的代碼另當別論

另外我覺得應該不會有多少人在比賽時候還有那心思去想著混淆代碼。。。而且如果是TC的話似乎有針對故意混淆代碼的懲罰措施(雖然我還沒有聽說過實例),甚至模板過長,unused code超過了某個比例也會被警告的(這個是事實,我做TC是把平常用的模板截掉一大段沒用的才交上去)

/*

還有一些比較腹黑的人可能喜歡用這樣的宏定義。。。

#define int long long

這種一定要注意啊。。。沒仔細看以為對方會爆int就直接去challenge/hack的哭去吧

*/


在 C++ 里,宏剩下的無法代替的用途主要是:

1. 條件編譯;

2. 生成代碼。

在 C 裡面的話,就更多了:

模擬模板和泛型;

實現 C 中沒有自帶的控制流,如協程、異常;

創造新的語法。這個可以去看 Cello;

……

很多時候,我還覺得目前宏的功能不夠用。宏最主要的問題是:

1. 宏不是圖靈完備的;

2. 宏完全是字元串處理,缺乏編譯期的各種信息,如類型等。


舉個栗子

for (vector[xxx]::iterator it = yyy.begin(); it != yyy.end(); it++) … (普通代碼)

#define each(x,y) for(typeof(x)::iterator y = x.begin(); y != x.end(); y++)

each(yyy,it) … (不支持msvc)

for (auto it : yyy) … (c++11)


這是手速狗的自我修養。手速狗的代碼不僅大量充斥宏定義,連裡面的變數都不願取有兩個字母的。

至於代碼,為什麼需要別人懂,比賽中只要能A就好。

另外對於防hack,在cf中這是沒有必要的,因為中途可以修改。

然而在bc和tc這種賽後hack的,防hack也許是一種阻礙別人分比你高的方式?(誤,斜眼笑

福利點我,acmer/oi常用宏定義:http://paste.ubuntu.com/11542319

來自島娘&>?


用不用宏,跟是不是大牛沒關係,主要從可維護性來考慮。舉些例子(C語言):

一、

#ifndef __STDIO_H_

#define __STDIO_H_

(感謝Jianlong Liu兄台評論里指正)

#endif

這是一個非常典型的頭文件宏定義對,主要是防止頭文件的重複引用。

二、

#define CODE_NUM 50

a.c文件里

p-&>cnNum = CODE_NUM;

b.c文件里

p-&>cnNum = CODE_NUM;

假設要修改50為51,如果不用宏,需要一個一個去搜索50,改成51,很容易發生錯漏(想像一下上百萬行代碼,幾十個文件都設置成50的情況)。而用宏的話,只需要將宏改成

#define CODE_NUM 51

即可,方便又不容易出錯。

這也是很典型的用宏的情況。

三、

我在新建一個項目的時候,往往第一時間創建一個頭文件,裡面全是typedef。就是為了避免不同編譯器、不同平台對一些數據類型的定義不一致。

四、

#define DEBUG 1

#if DEBUG == 1

#define ASSERT(x,s) if(x){printf("%s in file %s, line %d.", s, __FILE__, __LINE__); while(1);}

#else

#define ASSERT(x,s)

#endif

這個宏主要用於程序調試的時候,如果程序出錯,可以很快定位程序問題所在。主要在嵌入式里用。當調試完成後,正式發布時,將DEBUG的定義成0即可。

五、

另外就是風格的統一了。比如Windows的頭文件經常有

#define VOID void

是為了與它的數據格式DWORD之類的風格統一。

(除了可維護性外,在C語言里也有為了加快運行速度的。不展開了。)

另外,題主說用宏的代碼難讀。這說明題主參加項目還是少。可閱讀性是代碼可維護性一個重要方面,有經驗的程序員,看到宏的命名,大概就能猜到寫程序的人的用意。宏恰恰大大增強了代碼的可閱讀性(連彙編都要用宏,難以想像沒有宏的代碼,簡直是噩夢!簡直是噩夢!! ),這一點,相信做過幾個項目,多看看代碼,就能有體會。

ps: 看到一些回答,說是給新人設置門檻。我想說,如果這也叫門檻,那程序員真的不適合你。


黑貓不是大牛,而且某種意義上是程序員們不齒的一種人,然而

有些非代碼本身的原因加上時間緊迫逼得加腦子笨你不得不用宏

今天一天就越到兩個例子:

1.考慮以下情景,黑貓需要實現一個叫hehe的功能,然而這個hehe有很多個變種,正常的想法要寫多個hehe函數,甚至創造一個hehe類,裡面塞了所有hehe函數封裝起來

double hehe(int a, int b, int c){
double something;
// there is 50000 line of code with chaos

return something;
}

double hehe2(int a, int b, int c)
{
double something;
// there is another 50000 line of code with chaos

return something;
}

double hehe3(int a, int b, int c){
double something;
// there is another 50000 line of code with chaos

return something;
}

//換行不換行混著用,程序員們來咬黑喵啊~

然而現在你有點趕時間,而且同一個出現暫時一種情形比如hehe2要用一段不長不短的時間,而且其他幾乎全部擱置。極端點,甚至有如下的情景:每次實現的功能(或者說是某個部件)是hehe種類之間的組合,3種hehe就有個3+3+1種組合(C13+C23+C33)那麼為了滿足所有這些組合,笨辦法是寫7個函數(比如hehe12,hehe23),聰明辦法是搞一個hehe模和hehe類,把要實現的功能定義成裡面的幾個邏輯變數。那麼再複雜一下,hehe的2情景里就有10種情景怎麼辦呢?

但是要是像黑貓這種不懂你們程序員浪漫的,又笨又沒有時間而且類似的業務真的是一段時間實現某一種組合然後先換成別的呢?其實上面已經提到做法了——邏輯變數,只不過我們要把它設成宏:

double hehe(int a, int b, int c)
{
double something;
#define STYLE_1 1
#if STYLE_1 ==1
// there is 50000 line of code with chaos
#endif

#if STYLE_1 ==2
// there is 50000 line of code with chaos
#endif

/// ... ...///

#define STYLE_2 1
#if STYLE_2 ==1
// there is 50000 line of code with chaos
#endif

#if STYLE_2 ==2
// there is 50000 line of code with chaos
#endif

/// ... ...///

#define STYLE_3 1

///
//...
//...
//...
return something;
}

現在就可以向選菜單一樣挑選需要的hehe種類,完成hehe組合來實現特定業務。每次運行只需要改幾個define就行了。

其實這裡,模版,多注釋都嘗試過。但是還是感覺在不同業務之間切換宏是最快的,選出自己要的組合,然後在源文件的開頭修改定義就能快速不看代碼,不改注釋(有些注釋該起來還相當複雜,並不是大段刪掉的)的方法

其實如果不是業務代碼邏輯的切換,而是同邏輯函數指針的切換,還真是更傾向於模版

2.情形2比上一個噁心多了,某個供應商給了你一個dll,裡面只有幾個固定函數,其中一個固定函數的輸入類型是他指定的 function object。 然而這個無良的供應商把源文件加密了,無法看到和修改這個這個需要function object的固定函數的內容。

因為該固定函數的固定和無法修改,即使能夠自己寫出我想要的 function_object功能,也不能額外的參數,因為固定函數調用該object的方法已經被他限死了:

fucking_encrypt(int a, int b , fucking_functionType* function_object)
//上面這個你只能看見頭文件

void function_object(int a,int b,/*一加入就報錯的*/ double c){//somgthing c..}
//({}都不換行了,反正不寫內容,你們程序員來咬黑貓啊~~~)

這個時候想黑貓這種笨人不會程序員那套騷氣操作的,第一個想到的念頭就是: 結構上動不了手腳,就在編譯上做文章,反正我需要的就是實現目標。

假設黑貓不需要面向用戶,自己寫的自己用,完全可以在IDE里修改參數,那麼:

#define Pseudo_c
void function_object(int a,int b){//使用Pseudo_c代替c..}

以上尤其是2的做法確實黑貓知道是有害的(上課時就講要盡量避免宏),而且其實之後進過高人指點又通過別的方法(全局變數排隊)實現了,但是當時時間有限趕,管他什麼騷招實現就好了。這些問題擠占太多時間了,粗暴一點能省一點就省一點,剩下的時間我都可以用來搞模型(程序員們不齒黑喵吧,喵哈哈哈哈~~~~~

說到底這些都是粗暴的用法,樓上大神們的用法還是比較正規的。想學(抄)習(寫)的同學其實標準庫里就有極佳的學習例子:c複數類,那裡面宏的使用可真是出神入化了

想到再補


還有一點,就是別人challenge/hack你的時候更加難了


這叫手動混淆/手動JIT


顯得逼格更高,讓新手望而卻步


首先要知道一點是宏和模板一樣是圖靈完備的,然後我準備show點代碼

第一種情況,我會創建很多棧對象,但是我並不想知道這個對象的name是什麼,生命周期一到自動消失,這時候我把當前行數拼接進去,像這樣

這套代碼會開啟一個資料庫事務,並且會用當前行作為SavePoint的name,省力不少,我至少不用去想到底改給個什麼字元串了。這樣的情況非常多。

這是一個調試器的代碼,他可以這麼使用,並且可以一直括弧下去,靠宏完成了這個技術

剩下的應用宏的地方,比如在寫CUDA之類的時候,宏都是第一選擇

所以,宏在好用的地方這麼好用,why not?

PS:宏的缺點也多,不在這說了.


寫的方便,運行的快。


這也只是個習慣,你要是用inline函數也是可以的,可以省去許多重複的代碼


無論什麼語言,都追求效率,復用和精簡。

在c++裡面,你有const,template,inline ...

在c裡面,你只有宏。

但是複雜的宏並不安全,並不靈活,並不容易理解。所以在c++中用複雜的宏是很不好的習慣,在c中則是沒有辦法。


最煩C++裡面用宏,調試麻煩,代碼可讀性差,邏輯容易亂


宏有兩種意義:

1. 用做常量、表達式、函數表達式的別名,使程序可讀性增強。例如下面這段程序:

#define MAX_LENGTH 200

int main(void)
{
char str[MAX_LENGTH] = "";

/* Something to do */
for (i = 0; i &< MAX_LENGTH; ++i) { /* Something to do */ } // for (i = 0 ...) /* Something to do */ } // main()

就將常量 200 賦予了意義。

2. 方便調試。如果某個量在某個時刻因調試需要改動,那麼這時候如果該常量用宏定義過,則只要改一遍宏的值就行了,而不需要到整個程序中去尋找各處該常量的值依次改變,費時間不說,還容易漏。


嵌入式產品的設計為例吧。

一個功能晶元的驅動,為了能在所有的相同控制晶元(也可能是同一系列)裡面通用,一個晶元的引腳連接到主晶元的I/O口,不同的產品是不同的,不用宏定義的話要改大片的代碼而且可讀性很差。

同樣的一個晶元的驅動,有時候它的指令就是一個十六進位數,你在看驅動代碼的時候看到的都是一個個十六進位數,看起來多費勁。比如可能寫入使能是0x2a,你看資料手冊能明白,可是過幾天你就不懂了。假設你把0x2a定義成Write_Enable,是不是一目了然?


醫生寫的字為什麼那麼草?


首先,不是除了自己都很難理解啊…像什麼rep gcd ll之類的幾乎已經可以說是約定俗成了→_→不太常見的話根據縮寫推測一下也能猜個八九不離十←_←至於個別反人類的定義不要看就好了o_O

其次,打這種比賽除了演算法正確性最重要的就是完成速度,把常用的代碼段寫好了放在那也是比較自然的想法嘛⊙0⊙


我也來補個刀

在Qt的部分源代碼中

Q_TRY

{

}

Q_CATCH(...)

{

}

在Use exception時,try和catch被正確替換,而不用exception時,try和catch會被替換為if(1)和???(忘了)

EASTL做的則要略微遜色一點

#ifdef EA_USE_EXCEPTION

try{

#endif

...

#ifdef ...

}

#endif

---

同時,Qt也有Q_NULLPTR, Q_NOEXCEPT, EASTL也有類似的設施(應該是為了兼容pre-C++11和after-C++11)

暫時想到這麼多,等會回來再補


推薦閱讀:

在 Windows 下鍵入 Enter 鍵,是在鍵盤緩衝區中存入
還是
兩個?

為什麼用C/C++編寫的程序只能用鍵盤輸入,而且輸出結果也只能在一個黑屏上顯示是一些字元?
現在C++11/14有很多公司在用嗎?
一段很有意思的代碼,你能說出為什麼結果是這樣嗎?
既然c++的非virtual的函數可以重定義,virtual函數相比非virtual的有什麼優勢?

TAG:編程 | C | CC | ACM競賽 |