具有左值和右值的可变参数模板 class 构造函数
Variadic template class constructor with lvalues and rvalues
我正在构建一个机器学习库,试图充分利用 C++ 的内置功能,尤其是 C++11。我有多种执行输入修改的 classes,称为 Transformations
。现在我想构建它们的管道,将它们一个接一个地链接起来(最终在链的末端有一个机器学习算法,如 classifer 或回归器)。
我认为带有可变模板参数的 class 是这个用例的完美匹配。问题是我想在构造函数中接受右值和左值。
如果是右值,我想移动它,如果是左值,我想保留对它的引用(尽管我仍然不能 100% 确定这一点,因为它可能是引用绑定到某个范围,并且作为函数的结果返回管道会爆炸;但是对于这个库的目的来说,这可以被记录下来)。
这将是 class:
template <class... Ts>
class Pipeline {
};
template <class T, class... Ts>
class Pipeline<T, Ts...> {
public:
Pipeline(T?? transformation, Ts ??... following) : Pipeline<Ts...>(following...), _transformation(???) {}
...
}
我不知道_transformation
是否应该是一个引用,初始化列表中是否std::move
以及T
和[=16=的类型应该是什么? ] 在构造函数中。
编辑:在左值的情况下,它应该是非常量,因为管道可以修改转换。
我不确定这是否符合您的要求
#include "iostream"
#include "string"
template <class... Ts>
class Pipeline {
};
template <class T, class... Ts>
class Pipeline<T&&, Ts...>: Pipeline<Ts...> {
T _transformation;
public:
Pipeline(T&& transformation, Ts... following) : Pipeline<Ts...>(std::forward<Ts>(following)...), _transformation(std::move(transformation)) {
std::cout << "rvalue " << _transformation << " " << transformation << std::endl;
}
};
template <class T, class... Ts>
class Pipeline<T&, Ts...>: Pipeline<Ts...> {
T& _transformation;
public:
Pipeline(T& transformation, Ts... following) : Pipeline<Ts...>(std::forward<Ts>(following)...), _transformation(transformation) {
std::cout << "lvalue " << _transformation << " " << transformation << std::endl;
}
};
int main() {
std::string param1 = "param1";
std::string param2 = "param2";
std::string param3 = "param3";
Pipeline<std::string&, std::string&&> p(param1, param2 + param3);
}
它输出:
rvalue param2param3
lvalue param1 param1
这是您可以执行的操作的示例(请注意,在下面的代码中,T
是一个转换,S
是管道):
#include<tuple>
#include<iostream>
struct T {
T(int i): v{i} { }
T(const T &t) { v = t.v; std::cout << "cpy ctor" <<std::endl; }
T(T &&t) { v = t.v; std::cout << "move ctor" <<std::endl; }
void operator()(int i) { std::cout << "operator(): " << (v+i) << std::endl; }
int v;
};
template<typename... T>
struct S {
static constexpr std::size_t N = sizeof...(T);
template<typename... U>
S(U&&... args): tup{std::forward<U>(args)...} { }
void operator()(int i) {
unpack(i, std::make_index_sequence<N>{});
}
private:
template<std::size_t... I>
void unpack(int i, std::index_sequence<I...>) {
exec(i, std::get<I>(tup)...);
}
template<typename U, typename... O>
void exec(int i, U &&u, O&&... o) {
u(i);
exec(i, o...);
}
void exec(int) { }
std::tuple<T...> tup;
};
int main() {
T t{40};
S<T, T> s{t, T{0}};
s(2);
}
基本思想是使用转发引用,这只有通过向构造函数提供自己的参数包才能实现。
在上面的示例中,移动了右值引用并复制了左值引用。否则,调用者将负责所引用对象的生命周期,这很容易出错。如评论中所述,如果需要,可以提交 std::ref
。
无论如何,您可以在构造函数中更改策略,因为那里有实际类型及其值。
为了避免继承,我使用了 tuple
来打包转换以备后用。因此,每当调用 operator()
时,它们的引用就会被删除。
我会用一些 sfinae 扩展 S
的构造函数来检查参数包(T
和 U
)是否相同。为此,您可以使用 std::is_same
的通用版本(如果需要,请参阅 here 了解可能的实现)。
显然这个例子是一个最小的例子。您可以在实际代码中使用多个转换,这是从类型 S<T, T>
切换到类型 S<T1, T2, TAndSoOn>
.
的问题
正如您通过执行上面的示例所看到的,在构造 S
时会正确调用复制和移动构造函数。 operator()
解压元组并使用引用,因此在这种情况下您没有额外的副本。
您可以按照以下方式做一些事情:
template <class... Ts>
class Pipeline {
};
template <class T, class... Ts>
class Pipeline<T, Ts...> {
public:
template<class U, class... Us>
Pipeline(U&& transformation, Us&&... following) : Pipeline<Ts...>(std::forward<Us>(following)...), _transformation(std::forward<U>(transformation)) {}
private:
TransformationWrapper<T> _transformation;
}
template<class T>
class TransformationWrapper {
public:
TransformationWrapper(T& t) : _reference(t) {}
TransformationWrapper(T&& t) : _ptr(new T(std::move(t))) {}
~TransformationWrapper() { delete _ptr; }
T& get() { if (_ptr==nullptr) return _reference.get() else return *_ptr; }
private:
std::reference_wrapper<T> _reference;
T* _ptr=nullptr;
}
但是每 get()
次转换都会花费您一个分支。
我正在构建一个机器学习库,试图充分利用 C++ 的内置功能,尤其是 C++11。我有多种执行输入修改的 classes,称为 Transformations
。现在我想构建它们的管道,将它们一个接一个地链接起来(最终在链的末端有一个机器学习算法,如 classifer 或回归器)。
我认为带有可变模板参数的 class 是这个用例的完美匹配。问题是我想在构造函数中接受右值和左值。
如果是右值,我想移动它,如果是左值,我想保留对它的引用(尽管我仍然不能 100% 确定这一点,因为它可能是引用绑定到某个范围,并且作为函数的结果返回管道会爆炸;但是对于这个库的目的来说,这可以被记录下来)。
这将是 class:
template <class... Ts>
class Pipeline {
};
template <class T, class... Ts>
class Pipeline<T, Ts...> {
public:
Pipeline(T?? transformation, Ts ??... following) : Pipeline<Ts...>(following...), _transformation(???) {}
...
}
我不知道_transformation
是否应该是一个引用,初始化列表中是否std::move
以及T
和[=16=的类型应该是什么? ] 在构造函数中。
编辑:在左值的情况下,它应该是非常量,因为管道可以修改转换。
我不确定这是否符合您的要求
#include "iostream"
#include "string"
template <class... Ts>
class Pipeline {
};
template <class T, class... Ts>
class Pipeline<T&&, Ts...>: Pipeline<Ts...> {
T _transformation;
public:
Pipeline(T&& transformation, Ts... following) : Pipeline<Ts...>(std::forward<Ts>(following)...), _transformation(std::move(transformation)) {
std::cout << "rvalue " << _transformation << " " << transformation << std::endl;
}
};
template <class T, class... Ts>
class Pipeline<T&, Ts...>: Pipeline<Ts...> {
T& _transformation;
public:
Pipeline(T& transformation, Ts... following) : Pipeline<Ts...>(std::forward<Ts>(following)...), _transformation(transformation) {
std::cout << "lvalue " << _transformation << " " << transformation << std::endl;
}
};
int main() {
std::string param1 = "param1";
std::string param2 = "param2";
std::string param3 = "param3";
Pipeline<std::string&, std::string&&> p(param1, param2 + param3);
}
它输出:
rvalue param2param3
lvalue param1 param1
这是您可以执行的操作的示例(请注意,在下面的代码中,T
是一个转换,S
是管道):
#include<tuple>
#include<iostream>
struct T {
T(int i): v{i} { }
T(const T &t) { v = t.v; std::cout << "cpy ctor" <<std::endl; }
T(T &&t) { v = t.v; std::cout << "move ctor" <<std::endl; }
void operator()(int i) { std::cout << "operator(): " << (v+i) << std::endl; }
int v;
};
template<typename... T>
struct S {
static constexpr std::size_t N = sizeof...(T);
template<typename... U>
S(U&&... args): tup{std::forward<U>(args)...} { }
void operator()(int i) {
unpack(i, std::make_index_sequence<N>{});
}
private:
template<std::size_t... I>
void unpack(int i, std::index_sequence<I...>) {
exec(i, std::get<I>(tup)...);
}
template<typename U, typename... O>
void exec(int i, U &&u, O&&... o) {
u(i);
exec(i, o...);
}
void exec(int) { }
std::tuple<T...> tup;
};
int main() {
T t{40};
S<T, T> s{t, T{0}};
s(2);
}
基本思想是使用转发引用,这只有通过向构造函数提供自己的参数包才能实现。
在上面的示例中,移动了右值引用并复制了左值引用。否则,调用者将负责所引用对象的生命周期,这很容易出错。如评论中所述,如果需要,可以提交 std::ref
。
无论如何,您可以在构造函数中更改策略,因为那里有实际类型及其值。
为了避免继承,我使用了 tuple
来打包转换以备后用。因此,每当调用 operator()
时,它们的引用就会被删除。
我会用一些 sfinae 扩展 S
的构造函数来检查参数包(T
和 U
)是否相同。为此,您可以使用 std::is_same
的通用版本(如果需要,请参阅 here 了解可能的实现)。
显然这个例子是一个最小的例子。您可以在实际代码中使用多个转换,这是从类型 S<T, T>
切换到类型 S<T1, T2, TAndSoOn>
.
正如您通过执行上面的示例所看到的,在构造 S
时会正确调用复制和移动构造函数。 operator()
解压元组并使用引用,因此在这种情况下您没有额外的副本。
您可以按照以下方式做一些事情:
template <class... Ts>
class Pipeline {
};
template <class T, class... Ts>
class Pipeline<T, Ts...> {
public:
template<class U, class... Us>
Pipeline(U&& transformation, Us&&... following) : Pipeline<Ts...>(std::forward<Us>(following)...), _transformation(std::forward<U>(transformation)) {}
private:
TransformationWrapper<T> _transformation;
}
template<class T>
class TransformationWrapper {
public:
TransformationWrapper(T& t) : _reference(t) {}
TransformationWrapper(T&& t) : _ptr(new T(std::move(t))) {}
~TransformationWrapper() { delete _ptr; }
T& get() { if (_ptr==nullptr) return _reference.get() else return *_ptr; }
private:
std::reference_wrapper<T> _reference;
T* _ptr=nullptr;
}
但是每 get()
次转换都会花费您一个分支。