Home > Mobile >  Const-correctness for nested Proxy Classes
Const-correctness for nested Proxy Classes

Time:12-02

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?

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

Demo

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

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

  • Related