【问题标题】:How can I combine an in place transformation, and a copy transformation?如何结合就地转换和复制转换?
【发布时间】:2015-11-23 16:37:25
【问题描述】:

我想将这两个功能组合成一个功能接口:

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

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

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

因此,我最终每次都创建了两个函数,并遵循我的一个约定,As_ 获取并返回一个副本,而没有 As_ 获取并返回一个引用。

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

我对它的外观没有任何要求,但我希望有一种方法,我不依赖我的 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"

或者也许我可以使用参考包装器做一些事情。
这就是我要问的可能的实现。

【问题讨论】:

  • 如果函数具有相同的名称 - 您希望编译器如何确定您想调用哪个函数?如果您保留不同的名称并避免重复自己 - 将 As_Transformed 实现为“制作副本”,调用“转换”并返回转换后的副本
  • @Rostislav 可以通过模板或函数重载保持相同的函数名。你的建议是我已经做的,但如果可能的话,我也想避免它。我已经考虑过输入的引用包装器,或用于模板调度的 COPY / REF 类型。我很好奇其他人是否有任何优雅的解决方案。
  • 这些函数做了两个不同的事情。我不希望它们具有相同的名称以减少混淆。
  • @AnonMail 从概念上讲,它们是相同的转换。一种是对输入参数进行适当的转换;另一个返回转换的副本。这是否是一个好的做法,值得商榷,但我想看看如何实现它。
  • 我认为任何人都可以回答您的问题,您需要展示要执行不同逻辑(复制/不复制)的示例用法。

标签: c++ function templates overloading c++17


【解决方案1】:

您可以通过在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

【讨论】:

  • 不应该是采用reference_wrapperTransform 返回T&amp; 而不是reference_wrapper?这意味着您必须在达到递归时ref,但这也意味着返回的数据不再神奇。
  • @Yakk 当然可以,但至少在std::string 中,链接无法按预期工作-Transform(Transform(std::ref(t)) 中的外部调用调用了第一个重载。在这种情况下,最好的可行函数选择有点令人费解(而且很有趣),但clang 和 gcc 都同意这是应该的。
  • @Yakk 我猜复制构造函数被认为是比转换构造更好的匹配。得挖一点来向自己确认这一点:)
  • @Yakk 好像第一个是左值转换,比用户自定义转换的第二种情况好。
  • 是的,T 胜过 reference_wrapper 的原因有很多(一方面,reference_wrapper 没有被推断出来)。如果他们想通过引用来做,他们应该重复std::ref:在我看来Transform(std::ref(Transform(std::ref(bob)))
【解决方案2】:

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

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

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);

【讨论】:

    【解决方案3】:

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

    #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 Stackoverflow
    // user Evan Teran: http://stackoverflow.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<--
    

    【讨论】:

      【解决方案4】:

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

      // 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); });
      

      【讨论】:

      • std::transform 在这里没有问题 :) Transform 只是一个函数名。
      • @LogicStuff:是的,我只是将它用作支持他所询问的两种类型的转换的函数(函数模板,如果你想获得技术)的示例。
      • 这对 STL 算法非常有用,但我的输入不是容器。它们是自定义数据类型和原始类型。
      • @TrevorHickey:它们不需要是容器。它们只需要是你有迭代器的东西。对于特定的原始类型,您可能有类似void transform(int *in, int *out); 之类的内容,情况基本相同:要进行就地转换,您为输入和输出传递相同的地址,但要创建一个副本,您需要传递单独的@ 987654326@s 用于输入和输出。
      【解决方案5】:

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

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

      伪代码如下所示:

      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 :)
      

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

      #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 的规范,如果您坚持返回的std::future,那么该方法的行为是正常的。但是如果直接调用std::async,就变成了阻塞操作。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2017-12-25
        • 1970-01-01
        • 1970-01-01
        • 2017-07-02
        • 2015-08-15
        • 2020-02-13
        相关资源
        最近更新 更多