I'm currently experimenting with various use cases for C++20 modules, taking advantage of the (somewhat) stable support for C++20 modules in CMake and major compilers.
My goal is to utilize a public member defined in a base class with module linkage while having a derived class that is exported. During my exploration, I've tried several approaches and observed that some of them work as expected, while others do not.
I've attempted to find information on this specific case in the C++20 standard document C++ Standard Draft, but I still have some limitations in my understanding of this issue.
To provide some context, I've organized my code into three files:
libr5.ixx (a primary interface unit)
libr5_internal.ixx (a partition of libr5)
main.cpp (the main file, which uses the exported entity from libr5)
main.cpp is unchanged for all three examples
// main.cpp
import libr5;
#include <iostream>
int main(int argc, char *argv[]) {
ClassInLibr5 libr5_class_instance {};
libr5_class_instance.int_value = 50;
std::cout << libr5_class_instance.int_value << std::endl;
std::cout << libr5_class_instance.double_value << std::endl;
return 0;
}
compile commands are unchanged for all three examples:
clang++ -std=c++20 -fprebuilt-module-path=. --precompile libr5_internal.cppm
clang++ -std=c++20 -fprebuilt-module-path=. --precompile -fmodule-file="libr5:internal=libr5_internal.pcm" libr5.cppm
clang++ -std=c++20 -fprebuilt-module-path=. libr5.pcm libr5_internal.pcm -c
clang++ -std=c++20 -fprebuilt-module-path=. main.cpp -c -o main.o
clang++ main.o libr5.o libr5_internal.o
Compiler information (Manual compiled version):
clang version 18.0.0git
Target: aarch64-unknown-linux-gnu
Thread model: posix
InstalledDir: /usr/local/bin
First attempt (compiled):
// libr5.cppm
export module libr5;
export import :internal;
export {
struct ClassInLibr5: Internal {
ClassInLibr5(): Internal() {}
};
}
// libr5_internal.cppm
export module libr5:internal;
struct Internal {
Internal(): int_value(0), double_value(0) {}
int int_value;
double double_value;
};
Second attempt (not compiled)
// libr5.cppm
export module libr5;
import :internal;
export {
struct ClassInLibr5: Internal {
ClassInLibr5(): Internal() {}
};
}
// libr5_internal.cppm
module libr5:internal;
struct Internal {
Internal(): int_value(0), double_value(0) {}
int int_value;
double double_value;
};
The compilation error I have in the second attempt is something like:
main.cpp:6:24: error: declaration of 'int_value' must be imported from module 'libr5' before it is required
6 | libr5_class_instance.int_value = 50;
| ^
libr5_internal.cppm:5:7: note: declaration here is not visible
5 | int int_value;
...
Third attempt (compiled)
// libr5.cppm
export module libr5;
import :internal;
export {
struct ClassInLibr5: Internal {
ClassInLibr5(): Internal() {}
using Internal::int_value;
using Internal::double_value;
};
}
// libr5_internal.cppm
module libr5:internal;
struct Internal {
Internal(): int_value(0), double_value(0) {}
int int_value;
double double_value;
};
The difference between first and second attempts is: in First attempt, class Internal
is defined in an interface partition unit, whereas the Second attempt defining such class in implementation partition unit. Both of them should(?) have module linkage only.
The difference between second and third attempts is: Third attempt explicitly 'using' those member in derived class where as the second does not.
So here comes to the question:
[1] Is the difference in First Attempt and Second Attempt a 'well-defined' behavior in c++20 standard? (aka, no undefined behavior)
[2] Is the Third Attempt a 'well-defined' approach to make the Second Attempt work?
If anyone could provide insights or direct me to relevant sections of the standard and provide some human understandable interpretation, I would greatly appreciate it.
export
ed declarations. If a member is public and the user is expected to talk to it, then that class ought to be exported to. It functions as part of the class's interface and therefore should not be hidden.namespace detail
idiom for such things.detail
namespace either. It's not an implementation detail; it's an actual part of the interface.decltype
and template argument deduction are powerful tools for obtaining access to entities you can’t name. That doesn’t stop it from being useful to indicate your intent, though.