C/C++宏/模板黑魔法生成唯一名称

46

宏是可以的。 模板也是可以的。 几乎任何可用的方法都可以。

这个例子是关于OpenGL的,但技术上与C++有关,不依赖于OpenGL的知识。

明确的问题:

我想要一个表达式E;在这里我不需要指定唯一的名称;这样就会在定义E的位置调用构造函数,并且在包含E的块结束时调用析构函数。

例如,请考虑以下代码:

class GlTranslate {
  GLTranslate(float x, float y, float z); {
    glPushMatrix();
    glTranslatef(x, y, z);
  }
  ~GlTranslate() { glPopMatrix(); }
};

手动解决方案:

{
  GlTranslate foo(1.0, 0.0, 0.0); // I had to give it a name
  .....
} // auto popmatrix

现在,我不仅对于glTranslate有这个问题,还有很多其他的PushAttrib/PopAttrib调用也是如此。我不想为每个变量想出一个唯一的名称。有没有一些技巧,比如宏模板......或其他什么东西,可以自动创建一个变量,在定义点调用其构造函数;并在块结束时调用其析构函数?

谢谢!


3
我不明白为什么想出一个独特的名字比执行一些复杂的宏调用更困难。 - anon
6
就我所知,曾经有一次我尝试了类似的方案。我发现只需要创建一个类似于你所说的具有 push/pop 功能的 Transformation 类,并且该类还包含调用翻译等函数的成员函数。这样你就只需要一个类,并且只在需要时进行推送。 - GManNickG
3
我认为答案是__LINE__或者__COUNTER__ :-) - anon
@UncleBens:你能详细说明一下吗? - GManNickG
2
@GMan:我只是建议(并不是完全认真)将块转换为单个逗号表达式。据我所知,在完整表达式评估之后(在work_to_be_done...调用之后),临时变量会被销毁=>不需要给实例命名,甚至不需要唯一的名称;) - UncleBens
显示剩余3条评论
5个回答

68

个人而言,我不会这样做,而是想出独特的名称。但如果您想这样做,一种方法是使用iffor的组合:

#define FOR_BLOCK(DECL) if(bool _c_ = false) ; else for(DECL;!_c_;_c_=true)

你可以这样使用它:
FOR_BLOCK(GlTranslate t(1.0, 0.0, 0.0)) {
  FOR_BLOCK(GlTranslate t(1.0, 1.0, 0.0)) {
    ...
  }
}

这些名称都在不同的作用域中,不会发生冲突。内部名称隐藏外部名称。在iffor循环中的表达式是常量,应该很容易被编译器优化。


如果您真的想传递一个表达式,可以使用ScopedGuard技巧(参见Most Important const),但需要更多的工作来编写它。但好的一面是,我们可以摆脱for循环,并让我们的对象评估为false

struct sbase { 
  operator bool() const { return false; } 
};

template<typename T>
struct scont : sbase { 
  scont(T const& t):t(t), dismiss() { 
    t.enter();
  }
  scont(scont const&o):t(o.t), dismiss() {
    o.dismiss = true;
  }
  ~scont() { if(!dismiss) t.leave(); }

  T t; 
  mutable bool dismiss;
};

template<typename T>
scont<T> make_scont(T const&t) { return scont<T>(t); }

#define FOR_BLOCK(E) if(sbase const& _b_ = make_scont(E)) ; else

然后,您需要提供适当的enterleave函数:

struct GlTranslate {
  GLTranslate(float x, float y, float z)
    :x(x),y(y),z(z) { }

  void enter() const {
    glPushMatrix();
    glTranslatef(x, y, z);
  }

  void leave() const {
    glPopMatrix();
  }

  float x, y, z;
};

现在你可以完全不需要在用户端命名就能编写它:

FOR_BLOCK(GlTranslate(1.0, 0.0, 0.0)) {
  FOR_BLOCK(GlTranslate(1.0, 1.0, 0.0)) {
    ...
  }
}

如果您想一次传递多个表达式,这有点棘手,但您可以编写一个作用于operator,的表达式模板来将所有表达式收集到scont中。

template<typename Derived>
struct scoped_obj { 
  void enter() const { } 
  void leave() const { } 

  Derived const& get_obj() const {
    return static_cast<Derived const&>(*this);
  }
};

template<typename L, typename R> struct collect 
  : scoped_obj< collect<L, R> > {
  L l;
  R r;

  collect(L const& l, R const& r)
    :l(l), r(r) { }
  void enter() const { l.enter(); r.enter(); }
  void leave() const { r.leave(); l.leave(); }
};

template<typename D1, typename D2> 
collect<D1, D2> operator,(scoped_obj<D1> const& l, scoped_obj<D2> const& r) {
  return collect<D1, D2>(l.get_obj(), r.get_obj());
}

#define FOR_BLOCK(E) if(sbase const& _b_ = make_scont((E))) ; else

您需要从scoped_obj<Class>继承RAII对象,如下所示。
struct GLTranslate : scoped_obj<GLTranslate> {
  GLTranslate(float x, float y, float z)
    :x(x),y(y),z(z) { }

  void enter() const {
    std::cout << "entering ("
              << x << " " << y << " " << z << ")" 
              << std::endl;
  }

  void leave() const {
    std::cout << "leaving ("
              << x << " " << y << " " << z << ")" 
              << std::endl;
  }

  float x, y, z;
};

int main() {
  // if more than one element is passed, wrap them in parentheses
  FOR_BLOCK((GLTranslate(10, 20, 30), GLTranslate(40, 50, 60))) {
    std::cout << "in block..." << std::endl;
  }
}

所有这些都不涉及虚函数,所涉及的函数对编译器来说是透明的。事实上,通过将上述的GLTranslate更改为向全局变量添加一个整数,并在离开时再次减去它,以及下面定义的GLTranslateE,我进行了一项测试:

// we will change this and see how the compiler reacts.
int j = 0;

// only add, don't subtract again
struct GLTranslateE : scoped_obj< GLTranslateE > {
  GLTranslateE(int x):x(x) { }

  void enter() const {
    j += x;
  }

  int x;
};

int main() {
  FOR_BLOCK((GLTranslate(10), GLTranslateE(5))) {
    /* empty */
  }
  return j;
}

实际上,在优化级别-O2下,GCC会输出以下内容:

main:
    sub     $29, $29, 8
    ldw     $2, $0, j
    add     $2, $2, 5
    stw     $2, $0, j
.L1:
    add     $29, $29, 8
    jr      $31

我没想到,它优化得相当不错!

9
我从BOOST_FOREACH宏中学到了这个技巧。 - Johannes Schaub - litb
2
@Johannes Schaub - litb:我有一种感觉,我似乎错过了什么,但是这个有什么问题呢?你的if-for结构更好在哪里?问题在于这两个都只允许我们在任何特定作用域点上仅使用一个对象,因此我们实际上需要多个不同的名称。我错过了什么? - Lazer
1
@eSKay,你的链接代码没有问题。我的if/for结构会扩展到与你手动块等效的代码。然而,对于我最后的解决方案,你不再需要名称,这正是提问者最终想要实现的。个人而言,我喜欢给那些反映它们目的的对象命名。例如,将一个对象放在原点的翻译,我会写成Trans toOrigin(-x, -y, -z);而不是只写Trans(-x, -y, -z)Trans foo(-x, -y, -z);这样无法传达变换的真实目的。 - Johannes Schaub - litb
3
Litb,你为外星人工作吗?你是个三重特工还是什么?你每天早上都要重新编译你的大脑吗?我感觉很糟糕。 - jokoon
3
约翰尼斯,你的模板魔法总是让我想到疯狂的点子!谢谢你提供这个,特别是最后一个,我打算用它做些好玩的事情... :) - Xeo
显示剩余3条评论

39

如果您的编译器支持__COUNTER__(大多数编译器都支持),您可以尝试:

// boiler-plate
#define CONCATENATE_DETAIL(x, y) x##y
#define CONCATENATE(x, y) CONCATENATE_DETAIL(x, y)
#define MAKE_UNIQUE(x) CONCATENATE(x, __COUNTER__)

// per-transform type
#define GL_TRANSLATE_DETAIL(n, x, y, z) GlTranslate n(x, y, z)
#define GL_TRANSLATE(x, y, z) GL_TRANSLATE_DETAIL(MAKE_UNIQUE(_trans_), x, y, z)

对于

{
    GL_TRANSLATE(1.0, 0.0, 0.0);

    // becomes something like:
    GlTranslate _trans_1(1.0, 0.0, 0.0);

} // auto popmatrix

1
您应该避免用下划线字符开头作为标识符。下划线开头的名称是保留给编译器的,如果您自己生成它们,可能会导致难以跟踪的名称冲突。(因此,例如,将“_trans_”替换为“trans_”或更独特的内容) - Magnus Hoff
13
@Magnus,GMan 使用“_trans”是可以的。这些名称仅在全局命名空间或std命名空间中保留。在任何地方都保留的名称是看起来像“_Trans”或“__trans”的名称。 - Johannes Schaub - litb
12
LINE 和 'COUNTER 都可以使用。 - Corwin
9
__LINE__ 的优势在于你可以在宏中再次引用该变量。而使用 __COUNTER__,它会再次递增。 - zneak
3
你可以通过注入另一个宏层来抵消这种影响,该层将存储生成的变量名。 - Thomas Eding
4
你好,如何做到这一点? - Claudiu

11

我认为现在可以像这样做:

struct GlTranslate
{
    operator()(double x,double y,double z, std::function<void()> f)
    {
        glPushMatrix(); glTranslatef(x, y, z);
        f();
        glPopMatrix();
    }
};

然后在代码中

GlTranslate(x, y, z,[&]()
{
// your code goes here
});

显然,需要使用C++11


7
我喜欢这个,你可以考虑将 std::function 改为模板以确保内联 :-) - Evan Teran
@EvanTeran 如果模板化,为什么可以进行内联? - antibus
1
std::function 使用类型擦除技术,这可以防止编译器在生成 GLTranslate::operator() 函数时“看到”传递的函数是什么。使用模板,它拥有完成内联所需的所有信息,因为(实际上),它为每个传递的唯一函数发出自定义版本的函数,并且因此可以安全地进行内联。 - Evan Teran

2

使用C++17,一个非常简单的宏可以带来直观的使用:

#define given(...) if (__VA_ARGS__; true)

并且可以嵌套:

given (GlTranslate foo(1.0, 0.0, 0.0))
{
    foo.stuff();

    given (GlTranslate foo(1.0, 2.0, 3.0))
    {
        foo.stuff();
        ...
    }
}

1
这真的让人想起了C++中类似于Python的with语句。非常好。 - dteod
1
这真的让人想起了C++中的with语句。非常好。 - undefined

0

如一位回答中所述,规范的方法是使用 lambda 表达式作为块,在 C++ 中您可以轻松编写模板函数

with<T>(T instance, const std::function<void(T)> &f) {
    f(instance);
}

并像这样使用它

with(GLTranslate(...), [] (auto translate) {
    ....
});

但是避免在您的作用域中定义名称的机制最常见的原因是长函数/方法会做很多事情。如果这种问题一直困扰着您,您可以尝试现代OOP /清洁代码风格,使用非常短的方法/函数。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接