Search code examples
c++oopc++20const-correctness

Const-correctness for nested Proxy Classes


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

Demo in Compiler Explorer

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?


Solution

  • 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;
    }
    

    Demo

    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.

    Demo