簡體   English   中英

具有零 arguments 的可變參數宏即使使用 ##__VA_ARGS__ 也無法編譯

[英]Variadic macros with zero arguments doesn't compile even with ##__VA_ARGS__

如果我嘗試編譯以下代碼:

template <typename... TArgs>
void Dummy(const TArgs &...args)
{
}

#define DUMMY(...) Dummy("Hello", ##__VA_ARGS__)

int main()
{
    DUMMY();
}

我收到以下編譯錯誤:

g++ -std=c++17 -O3 -Wall main.cpp && ./a.out
main.cpp: In function 'int main()':
main.cpp:6:48: error: expected primary-expression before ')' token
    6 | #define DUMMY(...) Dummy("Hello", ##__VA_ARGS__)
      |                                                ^
main.cpp:10:5: note: in expansion of macro 'DUMMY'
   10 |     DUMMY();
      |     ^~~~~

https://coliru.stacked-crooked.com/a/c9217ba86e7d24bd

當我至少添加一個參數時,代碼可以正常編譯:

template <typename... TArgs>
void Dummy(const TArgs &...args)
{
}

#define DUMMY(dummy, ...) Dummy(dummy, ##__VA_ARGS__)

int main()
{
    DUMMY(); // This is strange. Why does this compile?
    DUMMY(1);
    DUMMY(1, 2);
    DUMMY(1, 2, 3);
}

https://coliru.stacked-crooked.com/a/e30e14810d70f482

但我不確定它是否正確,因為DUMMY至少需要一個參數,但我傳遞了零。

使用零參數時,標准__VA_ARGS__不會刪除 trailing 。 ##__VA_ARGS__其刪除多余的,是一個GCC擴展。

此 GCC 擴展不起作用,因為您使用的是標准兼容模式-std=c++17 ,而不是-std=gnu++17

關於 C/C++ 宏的一個重要事實是,不可能在沒有參數的情況下調用它們,因為宏參數可以是空標記序列。

因此, DUMMY()使用單個空參數而不是零參數調用宏DUMMY 這解釋了為什么第二個例子有效,也解釋了為什么第一個例子會產生語法錯誤。

__VA_ARGS__沒有元素時, ##__VA_ARGS__ GCC 擴展會從, ##__VA_ARGS__刪除逗號。 但是單個空參數與沒有參數不同。 當您將DUMMY定義為#define DUMMY(...) ,您保證__VA_ARGS__至少有一個參數,因此,不會被刪除。

***注意:如果您沒有使用--std選項指定某些 ISO 標准,GCC 會對該規則進行例外處理。 在這種情況下,如果...是唯一的宏參數並且調用具有空參數,則,##__VA_ARGS__確實會刪除逗號。 CPP 手冊中的 Variadic Marcos 部分對此進行了說明

上面的解釋對於唯一的宏參數是可變參數參數的情況是模棱兩可的,因為試圖區分根本沒有參數是空參數還是缺失參數是沒有意義的。 CPP 在符合特定 C 標准時保留逗號。 否則,逗號將作為標准的擴展而刪除。

DUMMY#define DUMMY(x, ...) ,如果DUMMY僅使用一個參數調用,則__VA_ARGS將為空,其中包括調用DUMMY() (一個空參數)和DUMMY(0) (一個參數, 0 )。 請注意,標准 C 和 C++ 直到 C++20,都不允許這種調用; 它們要求至少有一個(可能是空的)參數對應於省略號。 不過,GCC 從未強加過此限制,無論--std設置如何,GCC 都會省略帶有,##__VA_ARGS__的逗號。

從 C++20 開始,您可以使用__VA_OPT__內置宏作為處理逗號(以及可能需要刪除的任何其他標點符號)的更標准方法。 __VA_OPT__也避免了上面用空參數提出的問題,因為它使用了不同的標准: __VA_OPT__(x)如果__VA_ARGS__包含至少一個標記,則擴展為x 否則,它將擴展為空序列。 因此, __VA_OPT__將按預期對這個問題中的宏進行工作。

我相信所有主要的編譯器現在都實現了__VA_OPT__ ,至少在它們的最新版本中是這樣。

由於某些原因(這可能是 GCC 錯誤),如果您只使用#define DUMMY(...)而沒有其他參數,那么##__VA_ARGS__將無法按預期工作(如果__VA_ARGS__為空,它將不會刪除逗號)。

僅當您使用-std=c++17編譯時,這才是正確的。 當您使用-std=gnu++17編譯時,不會發生這種情況。 但無論如何##__VA_ARGS__是GCC 擴展,帶有##__VA_ARGS__的代碼##__VA_ARGS__不能用-std=c++17編譯。 但是 GCC 允許在-std=c++17模式下使用 GCC 擴展,除非您設置-pedantic標志。 但似乎 GCC 擴展在-std=c++17-std=gnu++17模式下的工作方式不同。

但是可以解決這個問題:

#include <utility>

template <typename... TArgs>
void Dummy(const TArgs &...args)
{
}

namespace WA
{
    class stub_t {};

    stub_t ArgOrStub()
    {
        return {};
    }

    template <typename T>
    auto ArgOrStub(T &&t) -> decltype( std::forward<T>(t) )
    {
        return std::forward<T>(t);
    }

    template <typename... TArgs>
    void RemoveStubAndCallDummy(stub_t, TArgs &&...args)
    {
        Dummy(std::forward<TArgs>(args)...);
    }

    template <typename... TArgs>
    void RemoveStubAndCallDummy(TArgs &&...args)
    {
        Dummy(std::forward<TArgs>(args)...);
    }
}

#define DUMMY(first, ...) WA::RemoveStubAndCallDummy( WA::ArgOrStub(first), ##__VA_ARGS__ )

int main()
{
    DUMMY();
}

當您調用DUMMY()first參數將為空,並且在預處理之后我們將得到WA::ArgOrStub()它將返回stub_t稍后將被RemoveStubAndCallDummy的第一次重載RemoveStubAndCallDummy 它很笨重,但我找不到更好的解決方案。

C++20 引入了__VA_OPT__作為一種在 arguments 大於零的情況下可選地擴展可變參數宏中標記的方法。
這消除了對##__VA_ARGS__ GCC 擴展的需要。 如果您可以使用該版本的標准,那應該是一個優雅的、編譯器無關的解決方案。

序列__VA_OPT__(x)僅在可變參數宏的替換列表中合法,如果__VA_ARGS__非空則展開為 x,如果為空則展開為空。

所以你可以簡單地做:
#define DUMMY(...) Dummy("Hello" __VA_OPT__(,) __VA_ARGS__)

這是有關__VA_OPT__的精彩博客文章(以及更多關於預處理器宏的內容): https://www.scs.stanford.edu/~dm/blog/va-opt.html

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM