【问题标题】:template function returning void模板函数返回 void
【发布时间】:2015-02-19 06:11:05
【问题描述】:

我的日志记录类中有以下功能:

template<class T> 
inline T ErrLog(T ret, const char* Format, ...)
{
    va_list args; va_start( args, Format ); _vsnprintf(mypWorkBuffer, MaxLogLength, Format, args); va_end(args);
    // do some fancy logging with mypWorkBuffer
    return ret;
}

(mypWorkBuffer 在别处定义)

这对我来说是一个非常方便的快捷方式,因为我可以记录一个错误并在一行中退出,这通过排除错误处理使代码更具可读性:

int f(x) {
   if (x<0) return ErrLog(-1, "f error, %d too small", x);
   ...
}

(而不是

int f(x) {
   if (x<0) {
      Log("f error, %d too small", x);
      return -1;
   }
   ...
}

)

我遇到的问题是如果 f 返回 void。我想做

void f(x) {
   if (x<0) return ErrLog(void, "f error, %d too small", x);
   ...
}

但这不会编译。

我想到了专业化,那就是加:

inline void ErrLog(const char* Format, ...)
{
    va_list args; va_start( args, Format ); _vsnprintf(mypWorkBuffer, MaxLogLength, Format, args); va_end(args);
    // do some fancy logging with mypWorkBuffer
    return;
}

这让我可以做

return ErrLog("f error, %d too small", x);

但是我不确定返回 char* 的函数是否安全。例如考虑:

char* f(x) {
   if (x<0) return ErrLog("error", "f error , %d too small", x);
   ...
}

我认为这个会匹配模板和专业化。

有什么想法/更好的解决方案吗?

【问题讨论】:

  • 为什么ret 甚至可以通过ErrLog?如果它没有被使用或修改,它就没有任何业务存在。当它是作为模板的唯一原因时,更是如此。
  • 你拼错了throw
  • etheranger - ret 被返回,这就是 n.m. 的重点。这里没有try catch throw,不知道你指的是什么
  • 你可以用不同的名字命名这两个函数。
  • “这里没有try catch throw”。这正是问题所在。

标签: c++ templates


【解决方案1】:

我认为只使用逗号操作符更惯用且简单得多:

inline void Errlog(const char *format,...) { /* ... */ }

void f(int x) {
    if (x<0) return Errlog("f error...");
}

double g(double x) {
    if (x<0) return Errlog("x is negative..."),-1.0;
    else return sqrt(x);
}

逗号运算符的左侧大小可以是void 表达式。

这完全消除了使用模板函数的需要。

编辑:如果你真的不喜欢使用逗号...

如果你真的想使用函数接口,你有三个选择。 给定一个通用函数(我在这里使用std::forward,以便我们可以将它与引用等一起使用):

template <class T>
inline T &&ErrLog(T &&ret,const char *format,...) {
    /* logging ... */
    return std::forward<T>(ret);
}

您可以:

1) 对 void 情况使用单独的函数。

void ErrLogV(const char *format,...) {
    /* logging ... */
}

void foo1(int x) {
    if (x<0) return ErrLogV("foo1 error");
}

2) 使用特殊的“标签”类型重载:

static struct errlog_void_t {} errlog_void;

inline void ErrLog(errlog_void_t,const char *format,...) {
    /* logging ... */
}

void foo2(int x) {
    if (x<0) return ErrLog(errlog_void,"foo2 error");
}

3) 或者使用 throw away 参数并强制转换为 void:

void foo3(int x) {
    // uses generic ErrLog():
    if (x<0) return (void)ErrLog(0,"foo3 error");
}

二次修改:为什么不带ret参数的版本不能工作

您可以使用ret 安全地定义版本;它不是模棱两可的,但它不一定会在你想要的时候使用。根本问题是,在一种情况下,您需要一种行为,而在另一种情况下,您需要另一种行为,但函数调用参数将具有完全相同的类型。

考虑以下示例:

#include <cstdarg>
#include <cstdio>
#include <utility>

using namespace std;

template <typename T>
T &&foo(T &&x,const char *format,...) {
    puts("T-version");
    va_list va;
    va_start(va,format);
    vprintf(format,va);
    va_end(va);
    puts("");
    return forward<T>(x);
}

void foo(const char *format,...) {
    puts("void-version");
    va_list va;
    va_start(va,format);
    vprintf(format,va);
    va_end(va);
    puts("");
}

int main() {
    foo("I want the void overload: %s, %s","some string","some other string");
    foo("I want to return this string","I want the const char * overload: %s","some string");
}

这将编译!但只会调用foo 的第一个版本。编译器无法将您的意图与参数类型区分开来。

为什么不模棱两可?

foo 的两个版本都将成为重载解决方案的候选者,但第一个版本将是更好的匹配。你可以参考一个detailed description of the process(尤其是关于排名的部分),但总之,当你有两个或多个const char *参数时,foo的第一个版本的const char *第二个参数比@第二个版本的省略号参数。

如果您只有一个 const char * 参数,那么返回 void 的版本将胜过通用版本,因为在其他条件相同的情况下,非模板重载优于模板重载:

foo("this will use the void-version");

简而言之,使用重载会编译,但会产生令人惊讶且难以调试的行为,并且无法处理void-returning 版本需要多个参数的情况。

【讨论】:

  • 我没有看到 OP 试图实现的目标,但逗号运算符?你为什么不把它放在一个块里?
  • 我相信 OP 正试图有一种方法来记录这些类型的错误返回情况,从而最大限度地减少代码混乱。例如,一个块不能用在另一个表达式的中间,并且根据大多数编码风格约定,会将一行替换为两到四行。
  • 是的,完全正确,我的代码旨在提供单行日志并返回,以避免将代码与错误处理块混淆 有趣的使用逗号!但这非常不寻常,而且 IMO 令人困惑,所以寻找更好的东西
  • 逗号很棒;每个人都应该知道它是如何工作的,这样他们就不会感到惊讶:) 尽管如此,还是用我认为是你的三个替代方案的修正答案,保持一个仅功能的界面。
  • @halfflat:逗号的另一种替代方法是让ErrLog 总是返回true 并写成if (error_condition &amp;&amp; ErrLog(fmt, ...)) return return_value;
【解决方案2】:

对于 void 问题,使用返回 0 的 int 特化,然后忽略它:

void f(x) {
   if (x<0) {
      ErrLog(0, "f error, %d too small", x);
      return;
   }
   ...
}

对于返回 char * 的函数,如果返回值是静态的,则可以使其安全(但由于您使用 C++,您也可以使用 std::string):

char* f(x) {
   static char err[] = "error";
   if (x<0) return ErrLog(err, "f error , %d too small", x);
   ...
}

编辑:

对于void 部分,我无法想象如何避免阻塞(独立于模板问题):

void g(int x);
void f(int x) {
    if (x<0) return g(x); // 2 errors here : g returns void and f returns void
}

如果函数 g 返回 void,则无论该函数是什么,都不能将其用作 return 语句的值。无论如何,您不能在返回void 的函数中使用return something;。我能想象的最好的(但这不是你问的)是:

void f(x) {
   if (x<0) ErrLog(0, "f error, %d too small", x);
   else {
      ...
   }
}

为 else 部分创建另一个块 ...

【讨论】:

  • 关键是要避免阻塞和单独的返回,所以如果 (x
  • @kofifus :我能理解你的愿望,但编译器不允许你在 void 返回函数中使用 return x; ...我已经编辑了我的帖子
  • 一个返回void的函数可以def返回另一个返回void的函数,所以你的f函数编译得很好(至少在VS2013中)
【解决方案3】:

我建议您使用具有用户定义的 Void 类型的模板特化来处理您的 Void 案例。 抱歉,这是 C++14,但你应该可以很容易地转换它,只需相应地更改返回类型

template<class T>
auto ErrLog(T ret, const char* Format, ...)
{
    return ret;
}

struct Void{ };

template<>
auto ErrLog(Void ret, const char* Format, ...)
{
    return;
}

int main()
{
    ErrLog(Void{}, "f error, %d too small");
    ErrLog(-1, "f error, %d too small");
}

另外我没有传递可变参数模板Args,这个方案更多是向你展示这个想法。

【讨论】:

  • 确实,只是更简洁一点,没有逗号运算符重载:-)
猜你喜欢
  • 2016-08-28
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-01-24
  • 2012-01-09
  • 2015-12-14
相关资源
最近更新 更多