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.
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.
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.
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.
How are other people solving this issue?
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.