What I need
I have a data structure Tree
with two levels of proxies one for Branch
and a more detailed Leaf
.
Here is a MCVE:
#include <vector>
#include <iostream>
class Tree {
public:
class Leaf;
class Branch {
public:
Branch(Tree* tree, int branch_id)
: tree_(tree), branch_id_(branch_id) {};
Leaf leaf(int leaf_id) {
return Leaf{ tree_, branch_id_, leaf_id };
}
float thickness() const {
return tree_->branch_thickness_[branch_id_];
}
void set_thickness(float thickness) {
tree_->branch_thickness_[branch_id_] = thickness;
}
private:
Tree* tree_;
int branch_id_;
};
class Leaf {
public:
Leaf(Tree* tree, int branch_id, int leaf_id)
: tree_(tree), branch_id_(branch_id), leaf_id_(leaf_id) {};
Branch branch() {
return Branch{ tree_, branch_id_ };
}
float color() const {
return tree_->leaf_color_[branch_id_][leaf_id_];
}
void set_color(float color) {
tree_->leaf_color_[branch_id_][leaf_id_] = color;
}
private:
Tree* tree_;
int branch_id_;
int leaf_id_;
};
Branch branch(int branch_id) {
return Branch{ this, branch_id };
}
Branch branch(int branch_id) const {
// Compile error:
// candidate constructor not viable: 1st argument ('const Tree *') would lose const qualifier
return Branch{ this, branch_id };
}
private:
std::vector<float> branch_thickness_{ 0.5 };
std::vector<std::vector<float>> leaf_color_{ {0.2, 0.4} };
};
void demo() {
Tree tree;
Tree::Branch branch = tree.branch(0);
Tree::Leaf leaf = branch.leaf(1);
std::cout << "Branch Thickness " << branch.thickness() << '\n';
std::cout << "Leaf Color " << leaf.color() << '\n';
branch.set_thickness(0.25);
std::cout << "Branch Thickness " << leaf.branch().thickness() << '\n';
}
void demo_const() {
const Tree tree;
Tree::Branch branch = tree.branch(0);
Tree::Leaf leaf = branch.leaf(1);
std::cout << "Branch Thickness " << branch.thickness() << '\n';
std::cout << "Leaf Color " << leaf.color() << '\n';
std::cout << "Branch Thickness " << leaf.branch().thickness() << '\n';
}
int main() {
demo();
demo_const();
return 0;
}
Compiling gives me the following error:
error : no matching constructor for initialization of 'Tree::Branch'
return Branch{ this, branch_id };
^ ~~~~~~~~~~~~~~~~~~~
note: candidate constructor not viable: 1st argument ('const Tree *') would lose const qualifier
Branch(Tree* tree, int branch_id)
^
note: candidate constructor (the implicit copy constructor) not viable: requires 1 argument, but 2 were provided
class Branch {
^
note: candidate constructor (the implicit move constructor) not viable: requires 1 argument, but 2 were provided
How do I get this to compile and both demo and demo_const to work.
What I tried so far
I read up on the topic and the pattern I found to template the reference to the tree und instanciate it for Tree
and const Tree
, as shown here and here.
template <TreeType>
class BranchTemplate {
public:
BranchTemplate(TreeType *tree, int branch_id)
tree_(tree),
branch_id_(branch_id)
{
}
private:
TreeType *tree_;
int branch_id_;
};
using Branch = BranchTemplate<Tree>;
using ConstBranch= BranchTemplate<const Tree>;
class Tree {
Branch branch(int branch_id)
{
return Branch(this, branch_id);
}
ConstBranch branch(int branch_id) const
{
return ConstBranch(this, branch_id);
}
}
While this works for Branch
I can't do the same for Leaf
as we cannot return a Leaf based on the constness of the branch, but this needs to depend on the constness of the template parameter. Adding more template parameters eventually lead to recursion issues.
Plan B
My plan B is to create two more independent classes ConstBranch and ConstLeaf. This however would duplicate the implementation, and might introduce subtle bugs when they are not consistent with each other.
Other ideas
I read about enable_if and think it might help here. However I couldn't find an example that made it clear how that would work.
Question
How are other people solving this issue?
CodePudding user response:
I don't know why you would need templates here. Providing const
overloads works just fine:
class Tree;
class Branch;
class Leaf {
public:
Leaf(Tree const* tree, int branch_id, int leaf_id) :
tree_(tree),
branch_id_(branch_id),
leaf_id_(leaf_id) {
}
Branch branch();
Branch const branch() const;
private:
Tree const* tree_;
int branch_id_;
int leaf_id_;
};
class Branch {
public:
Branch(Tree const* tree, int branch_id) :
tree_(tree),
branch_id_(branch_id) {
}
Leaf leaf(int leaf_id) {
return Leaf(tree_, branch_id_, leaf_id);
}
Leaf const leaf(int leaf_id) const {
return Leaf(tree_, branch_id_, leaf_id);
}
private:
Tree const* tree_;
int branch_id_;
};
Branch Leaf::branch() {
return Branch(tree_, branch_id_);
}
Branch const Leaf::branch() const {
return Branch(tree_, branch_id_);
}
class Tree {
public:
Branch branch(int branch_id) {
return Branch(this, branch_id);
}
Branch const branch(int branch_id) const {
return Branch(this, branch_id);
}
};
CodePudding user response:
Since your edit shows you want the tree_
members to match the const-ness of the Leaf
and Branch
objects, the template approach is now appropriate.
class Tree;
template<bool Const>
class BranchTemplate;
template<bool Const>
class LeafTemplate {
using TreeType = std::conditional_t<Const, Tree const, Tree>;
using BranchType = BranchTemplate<Const>;
public:
LeafTemplate(TreeType *tree, int branch_id, int leaf_id) :
tree_(tree),
branch_id_(branch_id),
leaf_id_(leaf_id) {
}
BranchType branch() const;
float color() const;
void set_color(float color);
private:
TreeType *tree_;
int branch_id_;
int leaf_id_;
};
using Leaf = LeafTemplate<false>;
using ConstLeaf = LeafTemplate<true>;
template<bool Const>
class BranchTemplate {
using TreeType = std::conditional_t<Const, Tree const, Tree>;
using LeafType = LeafTemplate<Const>;
public:
BranchTemplate(TreeType *tree, int branch_id) :
tree_(tree),
branch_id_(branch_id) {
}
LeafType leaf(int leaf_id) const;
float thickness() const;
void set_thickness(float thickness);
private:
TreeType *tree_;
int branch_id_;
};
using Branch = BranchTemplate<false>;
using ConstBranch = BranchTemplate<true>;
class Tree {
public:
Branch branch(int branch_id) {
return Branch(this, branch_id);
}
ConstBranch branch(int branch_id) const {
return ConstBranch(this, branch_id);
}
private:
std::vector<float> branch_thickness_{ 0.5 };
std::vector<std::vector<float>> leaf_color_{ {0.2, 0.4} };
friend LeafTemplate<true>;
friend LeafTemplate<false>;
friend BranchTemplate<true>;
friend BranchTemplate<false>;
};
template<bool Const>
auto LeafTemplate<Const>::branch() const -> BranchType {
return BranchType(tree_, branch_id_);
}
template<bool Const>
float LeafTemplate<Const>::color() const {
return tree_->leaf_color_[branch_id_][leaf_id_];
}
template<>
void LeafTemplate<false>::set_color(float color) {
tree_->leaf_color_[branch_id_][leaf_id_] = color;
}
template<bool Const>
auto BranchTemplate<Const>::leaf(int leaf_id) const -> LeafType {
return LeafType(tree_, branch_id_, leaf_id);
}
template<bool Const>
float BranchTemplate<Const>::thickness() const {
return tree_->branch_thickness_[branch_id_];
}
template<>
void BranchTemplate<false>::set_thickness(float thickness) {
tree_->branch_thickness_[branch_id_] = thickness;
}
Trying set_thickness
on a branch of a const Tree
will result in a linker error since it's just not defined. If you want a better error message, you can write a static_assert instead:
template<bool Const>
void BranchTemplate<Const>::set_thickness(float thickness) {
static_assert(!Const, "Cannot set branch thickness in a const tree");
tree_->branch_thickness_[branch_id_] = thickness;
}
I think having a bool Const
as template parameter instead of a typename TreeType
is easier, though you could stick with the TreeType
. You would just need to define LeafTemplate::BranchType
and BranchTemplate::LeafType
appropriately. No need for recursion.