C++11中如何传递指向临时变量的指针?

14

我有一个现有的函数:

void foo(const Key* key = nullptr)
{
  // uses the key
}

我想传递一个临时 Key 对象(即 rvalue)的指针,例如:

foo(&Key());

这会导致编译错误,但是在C++11/14中有没有一种方法可以实现这个?当然我可以这样做:

Key key;
foo(&key);

但我不需要对象键,我只需要它在 foo() 内部和 foo() 中

或者我可以这样做:

foo(new Key());

但是,这样对象就不会被删除。


6
加入 void foo(const Key &key){ foo(&key); } 如何? - nwp
3
你是否同时使用了三种不同的 C++ 标准? - alexeykuzmin0
3
@Detonar 我不认为有必要引入动态分配内存。 - François Andrieux
2
@AndreyRubliov:因为它并不是自然的,也不应该是。 - Nicol Bolas
4
不自然的做法是将指向临时对象的指针传递出去。 - Nicol Bolas
显示剩余5条评论
7个回答

11

我认为这不是一个好主意,但如果你真的非常想要一个临时的(temporary),并且不能更改foo,你可以将临时变量强制转换为const&

int main()
{
    foo(&static_cast<const Key&>(Key{}));
}

点击此处可在wandbox上查看实例


或者,您可以通过方便的函数“隐藏”对象的创建:

template <typename T, typename F, typename... Ts>
void invoke_with_temporary(F&& f, Ts&&... xs)
{
    T obj{std::forward<Ts>(xs)...};
    f(&obj);
}

int main()
{
    invoke_with_temporary<Key>(&foo);
}

wandbox.org上的实时示例


另一种选择:提供一个重载 foo 的引用:

void foo(Key&& key)
{
    foo(&key);
}

2
会导致未定义行为吗? - Andrey Rubliov
6
@AndreyRubliov 这个临时对象会一直存在,直到表达式结束,也就是在 foo 返回之后。它的地址在 foo 函数整个执行期间都是有效的。但如果存储该指针并在临时对象消失后继续使用它,则行为未定义。请注意不要改变原文意思。 - François Andrieux
4
@DavidHaim,我想“令人讨厌”是主观的。但是没有任何歧义或“记忆游戏”的玩法。我同意一个过载保护机制将是更好的解决方案,并且如果提出这个建议,这篇文章将会更好。但是这篇文章确实回答了提出的问题。 - François Andrieux
3
@DavidHaim:我的回答的第一句话就是“我不认为这是一个好主意,但如果你真的真的需要一个临时变量并且不能更改foo,你可以将临时变量转换为const&”。 - Vittorio Romeo
3
@DavidHaim:我不同意 - 那不是回答问题。 OP的问题很清楚,有一个答案,我已经提供了。是否这是一个好主意并不重要。显然在答案中提到这一点是一个好习惯。 - Vittorio Romeo
显示剩余6条评论

6

这是一个实用函数。它基本上是 std::move1 的反函数:

template<class T>
T& as_lvalue( T&& t ) { return t; }

如果使用不当,可能会导致悬空引用。
你的代码将变成:
foo(&as_lvalue(Key()));

"不能获取临时对象的地址"的目的是因为您可以通过隐式临时创建等行为获得极其意外的行为。
在这种情况下,我们明确地获取了临时对象的地址。
它并不比创建命名值、调用函数,然后立即丢弃命名值更危险。
1 std::move 接收一个左值或右值,并返回一个右值引用,表示消费者应将其视为一个即将被丢弃的临时值。 as_lvalue 接收一个左值或右值引用,并返回一个左值引用,表示消费者应将其视为一个持久存在的非临时值。
它们都是有效的操作,但 std::move 更为关键。(std::move 实际上可以称为 as_rvalue )。我建议不要使用像 unmove 这样的巧妙名称。

我喜欢这个,但我想知道是否有必要将其制作为const T& as_lvalue(const T&& t)?这不会与非右值编译,但如果您已经有左值,为什么要使用它呢?这还使得在非常量上下文中不应该使用它更清晰。或者我错过了什么吗? - mattnewport
@mattnewport 我认为在非const的情况下使用它没有问题。结果被丢弃,但是没有出现错误。有时候你只想要一个可写的值,并且可以被丢弃,而不必创建一个命名变量。std::move适用于rvalue和const lvalue,as_lvalue适用于lvalue,无论是否为const,以及rvalue,无论是否为const。 - Yakk - Adam Nevraumont
我同意这没有什么问题,只是我不确定它在实践中有多大用处。我猜想一个函数需要一个非可选的输出参数,而你并不感兴趣?我使用这种类型的情况主要是函数通过const指针获取非可选输入参数。 - mattnewport
3
这段话的意思是:一个函数通过指针接收一个可选的输出参数,如果存在则会执行不同的操作,因此您希望将它传递进去。或者,它通过指针接收一个非可选的输出参数,但您想要忽略它。另外,它也可以通过引用来实现相同的功能。或者,您正在与一个接受非常量指针作为输入参数的旧版API进行交互。此外,它还适用于可选的输入参数。所有这些情况都将随时间流逝而消逝,就像雨中的泪水一样。 - Yakk - Adam Nevraumont
好的,你说服我了 :) 我认为在将来我会使用&as_lvalue()而不是我在答案中描述的temp_ptr()方法。 - mattnewport

5

只需自行控制可丢弃变量的范围:

{
    Key key;
    foo(&key);
} // <-- 'key' is destroyed here

3
唯一需要使用指针而非引用的原因是如果指针可能为空。如果是这种情况,那么你的 foo 函数将会像这样:
void foo(const Key *key)
{
  if(key)
    //Do stuff with `key`
  else
    //Alternate code
}

考虑到这一点,您需要的是第二个重载函数,将所有“使用键执行操作”的内容重构为一个接受引用的函数。因此,请执行以下操作:

void foo(const Key &key)
{
  //Do stuff with `key`
}

void foo(const Key *key)
{
  if(key)
    foo(*key);
  else
    //Alternate code.
}

如果你有一些公共代码在两种情况下都会执行,也将其重构为自己的函数。

是的,“使用密钥进行操作”可能很复杂,并且分成了几行。但这表明这里存在非常奇怪的设计。

你也可以用另一种方法来重构它:

void foo(const Key &key) {foo(&key);}

1
我同意这是糟糕的API设计,但是有许多现有的遗留API,我们仍然必须使用const T*参数,即使对于非可选参数也是如此。请参见我的答案中的DirectX 11示例。为所有这些API编写包装器并不总是切实可行的。 - mattnewport
@mattnewport:但是你的D3D示例代码导致了非常丑陋的代码。如果一个函数调用语句需要10多行,那么肯定有问题。基本上,大多数D3D结构体不应该作为临时变量传递;它们有太多的成员。 - Nicol Bolas
D3D API的设计不幸地导致了丑陋的代码,但我仍然需要使用它。许多D3D“参数结构体”具有在许多情况下适用的合理默认值(这就是为什么它们提供方便的CD3D11_XXX辅助类),或者它们具有表示常见用途的特定(参数化)组合,例如索引缓冲区。如果我想编写一个返回这些内容的辅助函数,我仍然需要像temp_ptr()这样的东西。 - mattnewport

3

我使用类似这样的东西:

template <typename T>
const T* temp_ptr(const T&& x) { return &x; }

使用方式如下:

foo(temp_ptr(Key{}));

当处理某些传统API时,这非常有用。特别是DirectX 11经常通过const T*来聚合参数结构体,并且在创建和传递它们时很方便。我认为这种习惯用法没有任何问题,不像一些评论者所说的那样。尽管如此,我更喜欢那些API采用const引用并以不同的方式处理可选参数。

以下是一个D3D11 API调用的示例,其中这非常有用:

    vector<Vec3f> verts;
    ...
    ID3D11BufferPtr vbuf;
    d3dDevice->CreateBuffer(
        temp_ptr(CD3D11_BUFFER_DESC{byteSize(verts), D3D11_BIND_VERTEX_BUFFER}),
        temp_ptr(D3D11_SUBRESOURCE_DATA{data(verts)}), &vbuf);

要调用ID3D11Device::CreateBuffer()来创建顶点缓冲区。

在大型项目中,我可能会编写许多D3D API调用的包装器,使它们更方便以现代C ++风格调用,但对于我想要最小化额外代码或依赖项的小型独立示例项目,我发现这非常有用。

我过去使用的另一个技巧是:

foo(std::data({Key{}}));

但我不特别推荐这样做,因为我认为意图并不明确,并且需要对初始化列表的工作原理有过多了解。如果您需要传递临时“数组”,一种变体是有用的:

d3dDeviceContext->ClearRenderTargetView(rendertarget, data({0.0f, 0.0f, 0.0f, 0.0f}));

对于像ID3D11DeviceContext::ClearRenderTargetView()这样的API进行调用。


2

如果你关心的是变量作用域(如你在评论中提到的),你可以使用

{Key key;foo(&key);}


-2

Or I could do:

foo(new Key());

But then the object will not be deleted.

你可以创建一个智能指针,但我宁愿不这样做。虽然这仍然是一种可能性。
#include <memory>
foo(std::make_unique<const Key>().get());

1
@Veedrac,没有内存泄漏。临时智能指针将一直存在,直到完整表达式被评估,然后分配给创建的“const Key”的内存将被释放。 - YSC
糟糕,我把方法搞混了。不过,在这里进行堆分配是愚蠢的。 - Veedrac
@Veedrac 这正是我想说的。 - YSC
那么,为什么存在这个答案呢? - Veedrac

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