如何结合就地转换和复制转换?

How can I combine an in place transformation, and a copy transformation?

我想将这两个功能合并到一个功能界面中:

T& Transform(T & foo){
   //transform t
   return t;
}

T As_Transformed(T foo){
   //transform t
   return t;
}

有时我想转换传递给函数的变量。
其他时候我想要一个应用了转换的新变量。

因此,我最终每次都创建了两个函数,并遵循我的约定,其中 As_ 需要和 returns 一个副本,而不 As_ 需要和 returns 参考。

如何编写一个函数实现来处理这两种行为?

我对它的外观没有任何要求,但我希望有一种方法可以不依赖我的 As_ 约定,理想情况下我只创建一个函数而不是二.


示例:
这是一个示例。
我们取 Uppercase()As_Upercased()

std::string str1 = "hello";
Uppercase(str1); //str is now "HELLO"

std::string str2 = "hello";
auto str3 = As_Uppercased(str2); //str2 is still "hello", 
                                 //but str3 is "HELLO"

我不知道合并后的界面会是什么样子,但也许:

std::string str1 = "hello";
Uppercase<REF>(str1); //str is now "HELLO"

std::string str2 = "hello";
auto str3 = Uppercase<COPY>(str2); //str2 is still "hello", 
                                   //but str3 is "HELLO"

或者也许我可以用参考包装器做点什么。
这可能的实现方式,就是我要问的。

一种明显的可能性是为转换的源和目标传递迭代器(或范围)。如果您为源和目标传递相同的范围,它会原地转换。如果您传递单独的源和目标,则目标与源是分开的。

// copy transformation
std::transform(in.begin(), in.end(), std::back_inserter(out), 
    [](auto x) { return transformed(x); });


// in-place transformation:
std::transform(in.begin(), in.end(), in.begin(), 
    [](auto x) { return transformed(x); });

使用范围,您可以获得相同的基本效果,但语法开销要少得多,因此它们看起来像:

// copy transformation
transform(in, result, [](auto x) { return transformed(x); });

// in-place:
transform(in, in, [](auto x) { return transformed(x); });

不,您不能将它们组合起来,因为它们做的事情不同。一个修改传递给函数的对象,另一个保持原始对象不变。一个复制对象并 returns 它,另一个 returns 对原始对象的引用。那些东西是矛盾的要求。

但是,一个可以用另一个来实现,这样就不会有重复的逻辑。

T As_Transformed(T t){
   Transform(t);
   return t;
}

以上内容对于类似于 Transform 的任何其他函数都是相同的,因此,如果您想避免重复样板文件,您可以将其概括为模板并改用它:

template<class T, class Func>
auto copy_and_call(T copy, Func&& function) {
    function(copy);
    return copy;
}

你可以这样称呼它:

T t;
auto copy = copy_and_call(t, Transform);

话又说回来,如果您需要两种形式的函数数量很多但函数调用的数量很少,那么也许这并不能保证声明一个函数。您可以轻松地简单地在副本上使用 Transform 而不是在函数中这样做。下面代码不多

T t = "original"; // let's say T can be constructed from a string literal
T copy = t;
Transform(copy);

您可以告诉编译器如何通过在 std::reference_wrapper 上提供重载来区分两者。然后代码将如下所示,例如:

#include <iostream>
#include <functional>

using T = std::string;

T Transform(T t)
{
    t += " copy";
    return t;
}

std::reference_wrapper<T> Transform(std::reference_wrapper<T> t)
{
    t.get() += " ref";
    return t;
}

int main()
{
    T t{"original"};

    std::cout << Transform(Transform(t)) << "\n";
    std::cout << t << "\n";
    std::cout << Transform(Transform(std::ref(t))).get() << "\n";
    std::cout << t;
}

输出:

original copy copy
original
original ref ref
original ref ref

请注意,原始值如何在第一个调用链之后保持原样并在第二个调用链之后被修改。

在您的实际代码中,第一个重载只会调用第二个重载来转换其使用 std::ref 包装的复制传递参数以避免代码重复。

LIVE

我建议使用基于命名空间的替代方案:

#include <string>
#include <algorithm>

namespace inplace {
std::string& lowercase(std::string& src) {
  std::transform(src.begin(), src.end(), src.begin(), ::tolower);
  return src;
}
} // namespace inplace

std::string lowercase(std::string src) {
  return inplace::lowercase(src);
}

那样的话,函数名是相同的,它们的接口也是相同的,但是命名空间清楚地定义了你打算做什么:

int main() {
  std::string a = "ThIs Is A sTrInG";
  std::string b = "ThIs Is AnOtHeR sTrInG";

  // it is clear that I intend to lowercase-in-place
  inplace::lowercase(a);

  // I can document that, in the absence of namespace, all mutations
  // create a copy
  auto c = lowercase(b);

  std::cout << "a: " << a << "\n"
            << "b: " << b << "\n"
            << "c: " << c << "\n";
}

输出:

a: this is a string
b: ThIs Is AnOtHeR sTrInG
c: this is another string

请注意,用户不可能发出指令

using namespace inplace;

因为会造成歧义;这是编译错误的一部分:

error: call of overloaded 'lowercase(std::__cxx11::string&)' is ambiguous
   auto c = lowercase(b);
                       ^

因此,编译器将确保您必须使用inplace函数的完全限定名称。

以下代码显示了一组字符串突变及其对应的复制。

#include <iostream>
#include <string>
#include <algorithm>

namespace inplace {

bool is_not_space(char c) {
  return not std::isspace(c);
}

inline std::string& uppercase(std::string& src) {
  std::transform(src.begin(), src.end(), src.begin(), ::toupper);
  return src;
}

inline std::string& lowercase(std::string& src) {
  std::transform(src.begin(), src.end(), src.begin(), ::tolower);
  return src;
}

// Credit for the idea of ltrim, rtrim, and trim goes to Whosebug
// user Evan Teran: http://whosebug.com/users/13430/evan-teran
inline std::string& ltrim(std::string& src) {
  src.erase(src.begin(), std::find_if(src.begin(), src.end(), is_not_space));
  return src;
}

inline std::string& rtrim(std::string& src) {
  src.erase(std::find_if(src.rbegin(), src.rend(), is_not_space).base(), src.end());
  return src;
}

inline std::string& trim(std::string& src) {
  return ltrim(rtrim(src));
}

inline std::string& normalize(std::string& src) {
  return lowercase(trim(src));
}

}

// The create-a-copy versions simply forward the call to the in-place
// versions after having taken their argument by value.
inline std::string lowercase(std::string src) { return inplace::lowercase(src); }
inline std::string uppercase(std::string src) { return inplace::uppercase(src); }
inline std::string ltrim(std::string src)     { return inplace::ltrim(src);     }
inline std::string rtrim(std::string src)     { return inplace::rtrim(src);     }
inline std::string trim(std::string src)      { return inplace::trim(src);      }
inline std::string normalize(std::string src) { return inplace::normalize(src); }

int main() {

  std::string a = "ThIs Is A sTrInG";
  std::string b = "ThIs Is AnOtHeR sTrInG";

  // it is clear that I intend to lowercase-in-place
  inplace::lowercase(a);

  // I can document that, in the absence of namespace, all mutations
  // create a copy
  auto c = lowercase(b);

  std::cout << "a: " << a << "\n"
            << "b: " << b << "\n"
            << "c: " << c << "\n";

  std::string d = "     I NEED to normaliZE ThIs StrINg\r\n\t\t  ";
  std::string e = "\t\t\n\rAnD THIS one Too        \n\t\t     ";

  // again: transparent that I will do this in-place
  inplace::normalize(d);

  auto f = normalize(e);

  std::cout << "-->" << d << "<--\n"
            << "-->" << e << "<--\n"
            << "-->" << f << "<--\n";

}

结果:

a: this is a string
b: ThIs Is AnOtHeR sTrInG
c: this is another string
-->i need to normalize this string<--
-->
AnD THIS one Too
                     <--
-->and this one too<--

虽然我完全同意 Rostislav 的解决方案,但我还是忍不住提出一个 C++ 标准方法来解决这个问题。

想法是,您的 Transform 方法可以 return 一个代理对象来延迟转换的执行,而不是立即应用转换。如果代理对象被复制到您的原始类型,那么您可以在副本上应用转换。如果代理对象没有被复制而是被销毁,那么您将对原始对象应用转换。

伪代码如下所示:

struct Proxy {
    OriginalObject& ref;
    bool copied;
    operator OriginalObject() {
        OriginalObject copy(ref);
        transform copy;
        copied := true;
        return copy;
    }
    ~Proxy() { 
        if (not copied)
            transform original reference 
     }
};

Proxy Transform(OriginalObject& obj) { ... }

用法可能如下所示:

OriginalObject copy = Transform(myRef); // proxy gets copied
                                        // therefore transformed the copy
Transform(myRef);// proxy gets destructed, therefore transformed the original

///////////SPOILER ALERT/////////////////
{
    auto cynicCopy = Tranform(myRef);//proxy copied as proxy
    (myRef);//original is retained until proxy gets destroyed
} // proxy gets destroyed, original modified :)

我们可以删除代理的复制构造函数、复制赋值运算符、移动构造函数和移动赋值运算符 class 并对此感到高兴。但我喜欢愤世嫉俗的方式,所以我删除了复制方法但实现了移动方法。这是我的示例实现。

#include <iostream>
#include <string>

template<typename T, typename Operation>
//Proxy class for transformations
struct CynicTransform {
    std::reference_wrapper<T> ref;
    Operation op;
    bool opDtor;

    CynicTransform(T& r, Operation o)
        :
        ref(r),
        op(o),
        opDtor(true)
    { }

    CynicTransform(const CynicTransform&) = delete;
    CynicTransform& operator=(const CynicTransform&) = delete;

    CynicTransform(CynicTransform&& ct)
        :
        ref(ct.ref),
        op(ct.op),
        opDtor(ct.opDtor)
    {
        ct.opDtor = false;
    }

    CynicTransform& operator=(CynicTransform&& ct) {
        using std::swap;
        swap(ref, ct.ref);
        swap(op, ct.op);
        swap(opDtor, ct.opDtor);

        return *this;
    }

    ~CynicTransform() {
        if (opDtor) {
            op(ref.get());
        }
    }

    operator T() {
        T copy(ref.get());
        op(copy);
        opDtor = false;

        return copy;
    }
};

template<typename T, typename Operation>
//Provides ease of use for the proxy class
CynicTransform<T, Operation> MakeCynicTransform(T& tref, Operation op)
{
    return CynicTransform<T, Operation>(tref, op);
}

/*example implementation of a transformation method
 *   each of your transformation method may look like this
 */
auto Transform(std::string& strRef) 
{
    return MakeCynicTransform(strRef, [](std::string& strRef) {
        /*modification logic here*/
        strRef += " modified";
    });
}

int main() {
    std::string s("original");

    std::cout << "Initially: " << s << std::endl;

    std::string copy = Transform(s);

    std::cout << "Original: " << s << std::endl;
    std::cout << "Copy: " << copy << std::endl;

    {
        auto cynicCopy = Transform(s);

        std::cout << "Scope Original: " << s << std::endl;
    }
    std::cout << "Cynic Modified: " << s << std::endl;

    Transform(s);

    std::cout << "At Last: " << s << std::endl;
}

编辑:我的灵感来自 std::async 的规范,如果您坚持 returned std::future,那么该方法的行为是正常的。但是如果直接调用std::async,就变成了阻塞操作