将const_cast元素移出std :: initializer_list会有任何风险吗?

时间:2013-09-08 18:08:51

标签: c++ c++11

此问题建立在此@FredOverflow's question

之上
  

澄清:需要initializer_list方法,因为VC++2012 has a bug阻止了命名空间参数的转发扩展。 _MSC_VER <= 1700有错误。

我编写了一个可变参数模板函数,它可以折叠类型化容器中的任意数量的参数。我使用类型的构造函数将可变参数转换为可使用的值。例如。 _variant_t:)

我在MySql C ++库中为一次打击中的预准备语句推送参数时需要这个,而我的MySqlVariant将输入数据转换为MYSQL_BIND s。因为我可以使用BLOB,所以当我可以move&&周围的大容器时,我想尽可能地避免复制构造。

我做了一个简单的测试,注意到initialize_list对存储的元素执行copy-construct,并在超出范围时销毁它们。完美...然后我尝试将数据移出initializer_list,令我惊讶的是,它使用了lvalues而不是rvalues,正如我预期的那样std::move

  

有趣的是,就在Going Native 2013明确警告我移动不移动,前进不转发 ... 就像水,我的朋友之后 - 保持思维的深层。

但这并没有阻止我:)我决定const_cast initializer_list值并仍将其移出。需要强制执行驱逐令。这是我的实施

template <typename Output_t, typename ...Input_t>
inline Output_t& Compact(Output_t& aOutput, Input_t&& ...aInput){
    // should I do this? makes sense...
    if(!sizeof...(aInput)){
        return aOutput;
    }

    // I like typedefs as they shorten the code :)
    typedef Output_t::value_type Type_t;

    // can be either lvalues or rvalues in the initializer_list when it's populated.
    std::initializer_list<Type_t> vInput = { std::forward<Input_t>(aInput)... };

    // now move the initializer_list into the vector.
    aOutput.reserve(aOutput.size() + vInput.size());
    for(auto vIter(vInput.begin()), vEnd(vInput.end()); vIter != vEnd; ++vIter){
        // move (don't copy) out the lvalue or rvalue out of the initializer_list.
        // aOutput.emplace_back(std::move(const_cast<Type_t&>(*vIter))); // <- BAD!
        // the answer points out that the above is undefined so, use the below
        aOutput.emplace_back(*vIter); // <- THIS is STANDARD LEGAL (copy ctor)!
    }

    // done! :)
    return aOutput;
}

使用非常简单:

// You need to pre-declare the container as you could use a vector or a list...
// as long as .emplace_back is on duty!
std::vector<MySqlVariant> vParams;
Compact(vParams, 1, 1.5, 1.6F, "string", L"wstring",
    std::move(aBlob), aSystemTime); // MySql params :)

我还上传了一个完整的测试 on IDEone ^,显示std::string的内存使用此功能正常移动。 (我会把它全部粘贴在这里,但它有点长......)

只要_variant_t (或任何最终包装对象)具有正确的构造函数,它就很棒。如果数据可以移出,那就更好了。当我在正确的方向上测试它和std::move时,它几乎可以工作:)

我的问题很简单:

  • 我是按标准做的吗?
  • 事实上它是正确的还是只是副作用?
  • 如果std::move默认情况下initializer_list不起作用,那么我在这里做的是:非法,不道德,hacky ......或者只是错误的

PS :我是一名自学成才的Windows Native C++开发人员,对标准一无所知。
^我的借口,如果我在这里做非标准的事情。

更新

谢谢大家,我现在有答案和解决方案(短期和长期)

  

我喜欢SO的C ++ 11方面。 这里知识渊博的人......

3 个答案:

答案 0 :(得分:8)

在一般情况下,遗憾的是,这是未定义的行为。在§8.5.4/ 5,强调我的:

  

类型为std::initializer_list<E>的对象是从初始化列表构造的,就好像实现分配了N类型为const E 的临时数组,其中{ {1}}是初始化列表中的元素数。使用初始化列表的相应元素对该数组的每个元素进行复制初始化,   构造N对象以引用该数组。

如果您看到std::initializer_list<E>,则可以表现为std::initializer_list<E>

因此,当const E[N]离开const_cast时,您正在查看对const对象的可变引用。对const对象的任何修改都是未定义的行为。

当您移动const时,您正在修改std::string对象。不幸的是,未定义行为的行为之一似乎是正确的行为。但这在技术上是不确定的。

请注意,当您const进入另一个时, 是明确定义的,因为std::move(int) 只能复制,因此移动不执行任何操作,并且不会修改int个对象。但总的来说,它是未定义的。

答案 1 :(得分:1)

为分享我痛苦的人找到了另一种解决方案:

#if _MCS_VER <= 1700
// Use the code in the OP!
// VS 2012- stuff comes here.
#else
// VS 2013+ stuff comes here.
template <typename Output_t>
inline Output_t& Compact(Output_t& aOutput){
    return aOutput;
}

template <typename Output_t, typename First_t>
inline Output_t& Compact(Output_t& aOutput, const First_t& aFirst){
    aOutput.emplace_back(aFirst);
    return aOutput;
}

template <typename Output_t, typename First_t>
inline Output_t& Compact(Output_t& aOutput, First_t&& aFirst){
    aOutput.emplace_back(std::move(aFirst));
    return aOutput;
}

template <typename Output_t, typename First_t, typename ...Next_t>
inline Output_t& Compact(Output_t& aOutput, const First_t& aFirst, Next_t&& ...aNext){
    aOutput.emplace_back(aFirst);
    return Compact(aOutput, std::forward<Next_t>(aNext)...);
}

template <typename Output_t, typename First_t, typename ...Next_t>
inline Output_t& Compact(Output_t& aOutput, First_t&& aFirst, Next_t&& ...aNext){
    aOutput.emplace_back(std::move(aFirst));
    return Compact(aOutput, std::forward<Next_t>(aNext)...);
}
#endif // _MCS_VER <= 1700

PS VC ++ 2012 CTPnov2012有一个BUG阻止它在namespaced classes上工作。因此,没有{{1}的初始解决方案}必须做。 我的所有代码都是命名空间。 VC2013在理论上已经修复了......因此我会在升级时切换代码。

答案 2 :(得分:1)

您可以将专业化减少一个。这个“通用引用”专门化也应该包含左值引用,在这种情况下std::move将不执行任何操作。

template <typename Output_t, typename First_t>
inline Output_t& Compact(Output_t& aOutput, First_t&& aFirst){
    aOutput.emplace_back(std::forward<First_t>(aFirst));
    return aOutput;
}

资料来源:Scott Meyers在GoingNative2013上发表演讲;在this accu article

中详细说明